fix clippy warnings
This commit is contained in:
1 parent
7b88083020
commit
bdc3ba435f
39 files changed
+817
-680
No files matched your search
@@ -454,13 +454,9 @@ fn open_primary_device(
|
|||||||
.connected_outputs = scan.connected_outputs;
|
.connected_outputs = scan.connected_outputs;
|
||||||
let mut outputs = HashMap::new();
|
let mut outputs = HashMap::new();
|
||||||
for selection in scan.selections {
|
for selection in scan.selections {
|
||||||
let OutputSelection {
|
let crtc = selection.crtc;
|
||||||
connector,
|
let mode = selection.mode;
|
||||||
crtc,
|
let output = create_direct_output(state, &mut drm, &gbm, &renderer, selection)?;
|
||||||
mode,
|
|
||||||
settings,
|
|
||||||
} = selection;
|
|
||||||
let output = create_direct_output(state, &mut drm, &gbm, &renderer, connector, crtc, mode, &settings)?;
|
|
||||||
state
|
state
|
||||||
.direct_backend
|
.direct_backend
|
||||||
.as_mut()
|
.as_mut()
|
||||||
@@ -959,22 +955,9 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (_, selection) in selections {
|
for (_, selection) in selections {
|
||||||
let OutputSelection {
|
let crtc = selection.crtc;
|
||||||
connector,
|
let mode = selection.mode;
|
||||||
crtc,
|
match create_direct_output(state, &mut device.drm, &device.gbm, &device.renderer, selection) {
|
||||||
mode,
|
|
||||||
settings,
|
|
||||||
} = selection;
|
|
||||||
match create_direct_output(
|
|
||||||
state,
|
|
||||||
&mut device.drm,
|
|
||||||
&device.gbm,
|
|
||||||
&device.renderer,
|
|
||||||
connector,
|
|
||||||
crtc,
|
|
||||||
mode,
|
|
||||||
&settings,
|
|
||||||
) {
|
|
||||||
Ok(output) => {
|
Ok(output) => {
|
||||||
state
|
state
|
||||||
.direct_backend
|
.direct_backend
|
||||||
@@ -1419,11 +1402,14 @@ fn create_direct_output(
|
|||||||
drm: &mut DrmDevice,
|
drm: &mut DrmDevice,
|
||||||
gbm: &GbmDevice<DrmDeviceFd>,
|
gbm: &GbmDevice<DrmDeviceFd>,
|
||||||
renderer: &GlesRenderer,
|
renderer: &GlesRenderer,
|
||||||
connector: connector::Info,
|
selection: OutputSelection,
|
||||||
crtc: crtc::Handle,
|
|
||||||
mode: DrmMode,
|
|
||||||
settings: &OutputSettings,
|
|
||||||
) -> Result<DirectOutput, Box<dyn Error>> {
|
) -> Result<DirectOutput, Box<dyn Error>> {
|
||||||
|
let OutputSelection {
|
||||||
|
connector,
|
||||||
|
crtc,
|
||||||
|
mode,
|
||||||
|
settings,
|
||||||
|
} = selection;
|
||||||
let identity = connector_identity(drm, &connector);
|
let identity = connector_identity(drm, &connector);
|
||||||
let drm_surface = drm.create_surface(crtc, mode, &[connector.handle()])?;
|
let drm_surface = drm.create_surface(crtc, mode, &[connector.handle()])?;
|
||||||
let allocator = GbmAllocator::new(gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT);
|
let allocator = GbmAllocator::new(gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT);
|
||||||
@@ -1435,7 +1421,7 @@ fn create_direct_output(
|
|||||||
)?;
|
)?;
|
||||||
// Publish only after DRM/GBM creation succeeds; failed activation must not
|
// Publish only after DRM/GBM creation succeeds; failed activation must not
|
||||||
// leave a phantom output/workspace that could receive evacuated windows.
|
// leave a phantom output/workspace that could receive evacuated windows.
|
||||||
let (output, global) = create_output(state, &connector, mode, identity, settings);
|
let (output, global) = create_output(state, &connector, mode, identity, &settings);
|
||||||
let damage_tracker = OutputDamageTracker::from_output(&output);
|
let damage_tracker = OutputDamageTracker::from_output(&output);
|
||||||
let render_metrics = RenderMetrics::from_environment(output.name());
|
let render_metrics = RenderMetrics::from_environment(output.name());
|
||||||
|
|
||||||
|
|||||||
+33
-33
@@ -96,22 +96,22 @@ impl Config {
|
|||||||
theme: &ferese_config::theme::ResolvedTheme,
|
theme: &ferese_config::theme::ResolvedTheme,
|
||||||
inactive_dim: InactiveDimSettings,
|
inactive_dim: InactiveDimSettings,
|
||||||
) -> Result<ThemeSettings, ConfigError> {
|
) -> Result<ThemeSettings, ConfigError> {
|
||||||
let mut config = Self::default();
|
let config = Self {
|
||||||
config.theme = serde_json::from_value(serde_json::to_value(&theme.tokens).unwrap())
|
theme: serde_json::from_value(serde_json::to_value(&theme.tokens).unwrap())
|
||||||
.expect("resolved theme matches runtime schema");
|
.expect("resolved theme matches runtime schema"),
|
||||||
|
..Self::default()
|
||||||
|
};
|
||||||
let mut settings = config.theme_settings()?;
|
let mut settings = config.theme_settings()?;
|
||||||
settings.inactive_dim = inactive_dim;
|
settings.inactive_dim = inactive_dim;
|
||||||
Ok(settings)
|
Ok(settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn runtime_config(&self) -> Result<crate::RuntimeConfig, ConfigError> {
|
pub(crate) fn runtime_config(&self) -> Result<crate::RuntimeConfig, ConfigError> {
|
||||||
self.notifications.validate().map_err(ConfigError::InvalidBinding)?;
|
self.notifications.validate().map_err(ConfigError::Binding)?;
|
||||||
self.desktop_widgets.validate().map_err(ConfigError::InvalidBinding)?;
|
self.desktop_widgets.validate().map_err(ConfigError::Binding)?;
|
||||||
for daemon in &self.autostart {
|
for daemon in &self.autostart {
|
||||||
if daemon.command.first().is_none_or(|program| program.trim().is_empty()) {
|
if daemon.command.first().is_none_or(|program| program.trim().is_empty()) {
|
||||||
return Err(ConfigError::InvalidBinding(
|
return Err(ConfigError::Binding("autostart command must contain a program".into()));
|
||||||
"autostart command must contain a program".into(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let input_settings = self.input_settings()?;
|
let input_settings = self.input_settings()?;
|
||||||
@@ -217,29 +217,29 @@ pub enum ConfigError {
|
|||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
source: ferese_config::Error,
|
source: ferese_config::Error,
|
||||||
},
|
},
|
||||||
InvalidColumnWidth {
|
ColumnWidth {
|
||||||
field: &'static str,
|
field: &'static str,
|
||||||
value: String,
|
value: String,
|
||||||
},
|
},
|
||||||
InvalidAnimationValue {
|
AnimationValue {
|
||||||
field: &'static str,
|
field: &'static str,
|
||||||
value: f64,
|
value: f64,
|
||||||
},
|
},
|
||||||
InvalidLayoutValue {
|
LayoutValue {
|
||||||
field: &'static str,
|
field: &'static str,
|
||||||
value: f64,
|
value: f64,
|
||||||
},
|
},
|
||||||
InvalidInputValue {
|
InputValue {
|
||||||
field: &'static str,
|
field: &'static str,
|
||||||
value: String,
|
value: String,
|
||||||
},
|
},
|
||||||
InvalidBinding(String),
|
Binding(String),
|
||||||
InvalidWindowRule(String),
|
WindowRule(String),
|
||||||
InvalidThemeValue {
|
ThemeValue {
|
||||||
field: &'static str,
|
field: &'static str,
|
||||||
value: String,
|
value: String,
|
||||||
},
|
},
|
||||||
InvalidOutputProfile(String),
|
OutputProfile(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for ConfigError {
|
impl fmt::Display for ConfigError {
|
||||||
@@ -249,25 +249,25 @@ impl fmt::Display for ConfigError {
|
|||||||
Self::Parse { path, source } => {
|
Self::Parse { path, source } => {
|
||||||
write!(formatter, "failed to parse {}: {source}", path.display())
|
write!(formatter, "failed to parse {}: {source}", path.display())
|
||||||
}
|
}
|
||||||
Self::InvalidColumnWidth { field, value } => write!(
|
Self::ColumnWidth { field, value } => write!(
|
||||||
formatter,
|
formatter,
|
||||||
"invalid scrolling.{field} {value}; expected a positive number or \"full\""
|
"invalid scrolling.{field} {value}; expected a positive number or \"full\""
|
||||||
),
|
),
|
||||||
Self::InvalidAnimationValue { field, value } => {
|
Self::AnimationValue { field, value } => {
|
||||||
write!(formatter, "invalid animations.{field} value {value}")
|
write!(formatter, "invalid animations.{field} value {value}")
|
||||||
}
|
}
|
||||||
Self::InvalidLayoutValue { field, value } => {
|
Self::LayoutValue { field, value } => {
|
||||||
write!(formatter, "invalid layout.{field} value {value}")
|
write!(formatter, "invalid layout.{field} value {value}")
|
||||||
}
|
}
|
||||||
Self::InvalidInputValue { field, value } => {
|
Self::InputValue { field, value } => {
|
||||||
write!(formatter, "invalid input.{field} value {value}")
|
write!(formatter, "invalid input.{field} value {value}")
|
||||||
}
|
}
|
||||||
Self::InvalidBinding(message) => write!(formatter, "invalid binding: {message}"),
|
Self::Binding(message) => write!(formatter, "invalid binding: {message}"),
|
||||||
Self::InvalidWindowRule(message) => write!(formatter, "invalid window rule: {message}"),
|
Self::WindowRule(message) => write!(formatter, "invalid window rule: {message}"),
|
||||||
Self::InvalidThemeValue { field, value } => {
|
Self::ThemeValue { field, value } => {
|
||||||
write!(formatter, "invalid theme.{field} value {value}")
|
write!(formatter, "invalid theme.{field} value {value}")
|
||||||
}
|
}
|
||||||
Self::InvalidOutputProfile(message) => {
|
Self::OutputProfile(message) => {
|
||||||
write!(formatter, "invalid output profile: {message}")
|
write!(formatter, "invalid output profile: {message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,21 +279,21 @@ impl Error for ConfigError {
|
|||||||
match self {
|
match self {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
Self::Parse { source, .. } => Some(source),
|
Self::Parse { source, .. } => Some(source),
|
||||||
Self::InvalidColumnWidth { .. }
|
Self::ColumnWidth { .. }
|
||||||
| Self::InvalidAnimationValue { .. }
|
| Self::AnimationValue { .. }
|
||||||
| Self::InvalidLayoutValue { .. }
|
| Self::LayoutValue { .. }
|
||||||
| Self::InvalidInputValue { .. }
|
| Self::InputValue { .. }
|
||||||
| Self::InvalidBinding(_)
|
| Self::Binding(_)
|
||||||
| Self::InvalidWindowRule(_)
|
| Self::WindowRule(_)
|
||||||
| Self::InvalidThemeValue { .. }
|
| Self::ThemeValue { .. }
|
||||||
| Self::InvalidOutputProfile(_) => None,
|
| Self::OutputProfile(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn window_rules(&self) -> Result<Vec<WindowRule>, ConfigError> {
|
pub fn window_rules(&self) -> Result<Vec<WindowRule>, ConfigError> {
|
||||||
window_rules::validate(&self.window_rules).map_err(ConfigError::InvalidWindowRule)
|
window_rules::validate(&self.window_rules).map_err(ConfigError::WindowRule)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ pub(super) fn nonnegative_theme_value(value: f64, field: &'static str) -> Result
|
|||||||
if value.is_finite() && value >= 0.0 {
|
if value.is_finite() && value >= 0.0 {
|
||||||
Ok(value)
|
Ok(value)
|
||||||
} else {
|
} else {
|
||||||
Err(ConfigError::InvalidThemeValue {
|
Err(ConfigError::ThemeValue {
|
||||||
field,
|
field,
|
||||||
value: value.to_string(),
|
value: value.to_string(),
|
||||||
})
|
})
|
||||||
@@ -75,7 +75,7 @@ pub(super) fn finite_theme_value(value: f64, field: &'static str) -> Result<f64,
|
|||||||
if value.is_finite() {
|
if value.is_finite() {
|
||||||
Ok(value)
|
Ok(value)
|
||||||
} else {
|
} else {
|
||||||
Err(ConfigError::InvalidThemeValue {
|
Err(ConfigError::ThemeValue {
|
||||||
field,
|
field,
|
||||||
value: value.to_string(),
|
value: value.to_string(),
|
||||||
})
|
})
|
||||||
@@ -86,7 +86,7 @@ pub(super) fn unit_theme_value(value: f64, field: &'static str) -> Result<f64, C
|
|||||||
if value.is_finite() && (0.0..=1.0).contains(&value) {
|
if value.is_finite() && (0.0..=1.0).contains(&value) {
|
||||||
Ok(value)
|
Ok(value)
|
||||||
} else {
|
} else {
|
||||||
Err(ConfigError::InvalidThemeValue {
|
Err(ConfigError::ThemeValue {
|
||||||
field,
|
field,
|
||||||
value: value.to_string(),
|
value: value.to_string(),
|
||||||
})
|
})
|
||||||
@@ -96,7 +96,7 @@ pub(super) fn unit_theme_value(value: f64, field: &'static str) -> Result<f64, C
|
|||||||
pub(super) fn parse_color(value: &str, field: &'static str) -> Result<RgbaColor, ConfigError> {
|
pub(super) fn parse_color(value: &str, field: &'static str) -> Result<RgbaColor, ConfigError> {
|
||||||
let digits = value.strip_prefix('#').unwrap_or(value);
|
let digits = value.strip_prefix('#').unwrap_or(value);
|
||||||
if !digits.is_ascii() || !matches!(digits.len(), 6 | 8) {
|
if !digits.is_ascii() || !matches!(digits.len(), 6 | 8) {
|
||||||
return Err(ConfigError::InvalidThemeValue {
|
return Err(ConfigError::ThemeValue {
|
||||||
field,
|
field,
|
||||||
value: value.to_owned(),
|
value: value.to_owned(),
|
||||||
});
|
});
|
||||||
@@ -108,13 +108,13 @@ pub(super) fn parse_color(value: &str, field: &'static str) -> Result<RgbaColor,
|
|||||||
.zip(parse_channel(4))
|
.zip(parse_channel(4))
|
||||||
.map(|((red, green), blue)| (red, green, blue))
|
.map(|((red, green), blue)| (red, green, blue))
|
||||||
else {
|
else {
|
||||||
return Err(ConfigError::InvalidThemeValue {
|
return Err(ConfigError::ThemeValue {
|
||||||
field,
|
field,
|
||||||
value: value.to_owned(),
|
value: value.to_owned(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
let alpha = if digits.len() == 8 {
|
let alpha = if digits.len() == 8 {
|
||||||
parse_channel(6).ok_or_else(|| ConfigError::InvalidThemeValue {
|
parse_channel(6).ok_or_else(|| ConfigError::ThemeValue {
|
||||||
field,
|
field,
|
||||||
value: value.to_owned(),
|
value: value.to_owned(),
|
||||||
})?
|
})?
|
||||||
|
|||||||
@@ -23,16 +23,13 @@ impl Config {
|
|||||||
for configured in &self.bindings {
|
for configured in &self.bindings {
|
||||||
let identity = binding_identity(configured, &keymap)?;
|
let identity = binding_identity(configured, &keymap)?;
|
||||||
if !supplied.insert(identity.clone()) {
|
if !supplied.insert(identity.clone()) {
|
||||||
return Err(ConfigError::InvalidBinding(format!(
|
return Err(ConfigError::Binding(format!("duplicate binding {:?}", configured.keys)));
|
||||||
"duplicate binding {:?}",
|
|
||||||
configured.keys
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bindings.retain(|binding| binding_identity_for_runtime(binding) != identity);
|
bindings.retain(|binding| binding_identity_for_runtime(binding) != identity);
|
||||||
if configured.disabled {
|
if configured.disabled {
|
||||||
if configured.action.is_some() || configured.argument.is_some() {
|
if configured.action.is_some() || configured.argument.is_some() {
|
||||||
return Err(ConfigError::InvalidBinding(format!(
|
return Err(ConfigError::Binding(format!(
|
||||||
"disabled binding {:?} cannot have an action or argument",
|
"disabled binding {:?} cannot have an action or argument",
|
||||||
configured.keys
|
configured.keys
|
||||||
)));
|
)));
|
||||||
@@ -49,12 +46,10 @@ impl Config {
|
|||||||
pub(super) fn validate_commands(commands: &HashMap<String, Vec<String>>) -> Result<(), ConfigError> {
|
pub(super) fn validate_commands(commands: &HashMap<String, Vec<String>>) -> Result<(), ConfigError> {
|
||||||
for (name, argv) in commands {
|
for (name, argv) in commands {
|
||||||
if name.trim().is_empty() {
|
if name.trim().is_empty() {
|
||||||
return Err(ConfigError::InvalidBinding("command names cannot be empty".to_owned()));
|
return Err(ConfigError::Binding("command names cannot be empty".to_owned()));
|
||||||
}
|
}
|
||||||
if argv.is_empty() || argv[0].is_empty() {
|
if argv.is_empty() || argv[0].is_empty() {
|
||||||
return Err(ConfigError::InvalidBinding(format!(
|
return Err(ConfigError::Binding(format!("command {name:?} must contain a program")));
|
||||||
"command {name:?} must contain a program"
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +69,7 @@ pub(crate) fn physical_keymap(input: &InputSettings) -> Result<xkb::Keymap, Conf
|
|||||||
options,
|
options,
|
||||||
xkb::KEYMAP_COMPILE_NO_FLAGS,
|
xkb::KEYMAP_COMPILE_NO_FLAGS,
|
||||||
)
|
)
|
||||||
.ok_or_else(|| ConfigError::InvalidBinding("failed to compile the XKB keymap".to_owned()))
|
.ok_or_else(|| ConfigError::Binding("failed to compile the XKB keymap".to_owned()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn parse_binding(
|
pub(super) fn parse_binding(
|
||||||
@@ -83,7 +78,7 @@ pub(super) fn parse_binding(
|
|||||||
keymap: &xkb::Keymap,
|
keymap: &xkb::Keymap,
|
||||||
) -> Result<Binding, ConfigError> {
|
) -> Result<Binding, ConfigError> {
|
||||||
if configured.disabled {
|
if configured.disabled {
|
||||||
return Err(ConfigError::InvalidBinding(format!(
|
return Err(ConfigError::Binding(format!(
|
||||||
"disabled binding {:?} cannot be executed",
|
"disabled binding {:?} cannot be executed",
|
||||||
configured.keys
|
configured.keys
|
||||||
)));
|
)));
|
||||||
@@ -93,7 +88,7 @@ pub(super) fn parse_binding(
|
|||||||
let action = configured
|
let action = configured
|
||||||
.action
|
.action
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.ok_or_else(|| ConfigError::InvalidBinding(format!("binding {:?} has no action", configured.keys)))?;
|
.ok_or_else(|| ConfigError::Binding(format!("binding {:?} has no action", configured.keys)))?;
|
||||||
let action = parse_action(action, configured.argument.as_deref(), commands)?;
|
let action = parse_action(action, configured.argument.as_deref(), commands)?;
|
||||||
let trigger = match identity.match_mode {
|
let trigger = match identity.match_mode {
|
||||||
BindingMatch::Keysym => BindingTrigger::Keysym(identity.key),
|
BindingMatch::Keysym => BindingTrigger::Keysym(identity.key),
|
||||||
@@ -123,7 +118,7 @@ pub(super) fn binding_identity(
|
|||||||
let (modifiers, key) = parse_chord(&configured.keys)?;
|
let (modifiers, key) = parse_chord(&configured.keys)?;
|
||||||
if let Some((fingers, direction)) = parse_swipe(&key) {
|
if let Some((fingers, direction)) = parse_swipe(&key) {
|
||||||
if configured.match_mode == BindingMatch::Physical || modifiers != BindingModifiers::default() {
|
if configured.match_mode == BindingMatch::Physical || modifiers != BindingModifiers::default() {
|
||||||
return Err(ConfigError::InvalidBinding(
|
return Err(ConfigError::Binding(
|
||||||
"swipes cannot use keyboard modifiers or physical key matching".into(),
|
"swipes cannot use keyboard modifiers or physical key matching".into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -139,13 +134,13 @@ pub(super) fn binding_identity(
|
|||||||
.key_by_name(&key.to_ascii_uppercase())
|
.key_by_name(&key.to_ascii_uppercase())
|
||||||
.map(Keycode::raw)
|
.map(Keycode::raw)
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
ConfigError::InvalidBinding(format!(
|
ConfigError::Binding(format!(
|
||||||
"unknown XKB physical key name {key:?} in {:?}",
|
"unknown XKB physical key name {key:?} in {:?}",
|
||||||
configured.keys
|
configured.keys
|
||||||
))
|
))
|
||||||
})?,
|
})?,
|
||||||
BindingMatch::Swipe => {
|
BindingMatch::Swipe => {
|
||||||
return Err(ConfigError::InvalidBinding(
|
return Err(ConfigError::Binding(
|
||||||
"gesture keys must use Swipe3Left/Right/Up/Down (3–5 fingers)".into(),
|
"gesture keys must use Swipe3Left/Right/Up/Down (3–5 fingers)".into(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -178,7 +173,7 @@ pub(super) fn parse_chord(chord: &str) -> Result<(BindingModifiers, String), Con
|
|||||||
|
|
||||||
for component in chord.split('+').map(str::trim) {
|
for component in chord.split('+').map(str::trim) {
|
||||||
if component.is_empty() {
|
if component.is_empty() {
|
||||||
return Err(ConfigError::InvalidBinding(format!("invalid key chord {chord:?}")));
|
return Err(ConfigError::Binding(format!("invalid key chord {chord:?}")));
|
||||||
}
|
}
|
||||||
|
|
||||||
let slot = match component.to_ascii_lowercase().as_str() {
|
let slot = match component.to_ascii_lowercase().as_str() {
|
||||||
@@ -190,17 +185,17 @@ pub(super) fn parse_chord(chord: &str) -> Result<(BindingModifiers, String), Con
|
|||||||
};
|
};
|
||||||
if let Some(slot) = slot {
|
if let Some(slot) = slot {
|
||||||
if *slot {
|
if *slot {
|
||||||
return Err(ConfigError::InvalidBinding(format!("duplicate modifier in {chord:?}")));
|
return Err(ConfigError::Binding(format!("duplicate modifier in {chord:?}")));
|
||||||
}
|
}
|
||||||
*slot = true;
|
*slot = true;
|
||||||
} else if key.replace(component.to_owned()).is_some() {
|
} else if key.replace(component.to_owned()).is_some() {
|
||||||
return Err(ConfigError::InvalidBinding(format!(
|
return Err(ConfigError::Binding(format!(
|
||||||
"key chord {chord:?} contains more than one key"
|
"key chord {chord:?} contains more than one key"
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let key = key.ok_or_else(|| ConfigError::InvalidBinding(format!("key chord {chord:?} does not contain a key")))?;
|
let key = key.ok_or_else(|| ConfigError::Binding(format!("key chord {chord:?} does not contain a key")))?;
|
||||||
Ok((modifiers, key))
|
Ok((modifiers, key))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,7 +231,7 @@ pub(super) fn parse_keysym(name: &str) -> Result<u32, ConfigError> {
|
|||||||
symbol = xkb::keysym_from_name(&normalized, xkb::KEYSYM_CASE_INSENSITIVE);
|
symbol = xkb::keysym_from_name(&normalized, xkb::KEYSYM_CASE_INSENSITIVE);
|
||||||
}
|
}
|
||||||
if symbol.raw() == keysyms::KEY_NoSymbol {
|
if symbol.raw() == keysyms::KEY_NoSymbol {
|
||||||
Err(ConfigError::InvalidBinding(format!("unknown keysym {name:?}")))
|
Err(ConfigError::Binding(format!("unknown keysym {name:?}")))
|
||||||
} else {
|
} else {
|
||||||
Ok(symbol.raw())
|
Ok(symbol.raw())
|
||||||
}
|
}
|
||||||
@@ -249,7 +244,7 @@ pub(super) fn parse_action(
|
|||||||
) -> Result<BindingAction, ConfigError> {
|
) -> Result<BindingAction, ConfigError> {
|
||||||
let no_argument = || {
|
let no_argument = || {
|
||||||
if argument.is_some() {
|
if argument.is_some() {
|
||||||
Err(ConfigError::InvalidBinding(format!(
|
Err(ConfigError::Binding(format!(
|
||||||
"action {action:?} does not accept an argument"
|
"action {action:?} does not accept an argument"
|
||||||
)))
|
)))
|
||||||
} else {
|
} else {
|
||||||
@@ -257,7 +252,7 @@ pub(super) fn parse_action(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let required_argument =
|
let required_argument =
|
||||||
|| argument.ok_or_else(|| ConfigError::InvalidBinding(format!("action {action:?} requires an argument")));
|
|| argument.ok_or_else(|| ConfigError::Binding(format!("action {action:?} requires an argument")));
|
||||||
|
|
||||||
match action {
|
match action {
|
||||||
"none" => {
|
"none" => {
|
||||||
@@ -276,7 +271,7 @@ pub(super) fn parse_action(
|
|||||||
let command = required_argument()?;
|
let command = required_argument()?;
|
||||||
let argv = commands
|
let argv = commands
|
||||||
.get(command)
|
.get(command)
|
||||||
.ok_or_else(|| ConfigError::InvalidBinding(format!("unknown command {command:?}")))?;
|
.ok_or_else(|| ConfigError::Binding(format!("unknown command {command:?}")))?;
|
||||||
Ok(BindingAction::Spawn(argv.clone()))
|
Ok(BindingAction::Spawn(argv.clone()))
|
||||||
}
|
}
|
||||||
"close" => {
|
"close" => {
|
||||||
@@ -332,7 +327,7 @@ pub(super) fn parse_action(
|
|||||||
no_argument()?;
|
no_argument()?;
|
||||||
Ok(BindingAction::ToggleKeybindingGuide)
|
Ok(BindingAction::ToggleKeybindingGuide)
|
||||||
}
|
}
|
||||||
_ => Err(ConfigError::InvalidBinding(format!("unknown action {action:?}"))),
|
_ => Err(ConfigError::Binding(format!("unknown action {action:?}"))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,14 +337,14 @@ pub(super) fn parse_direction(argument: &str) -> Result<Direction, ConfigError>
|
|||||||
"right" => Ok(Direction::Right),
|
"right" => Ok(Direction::Right),
|
||||||
"up" => Ok(Direction::Up),
|
"up" => Ok(Direction::Up),
|
||||||
"down" => Ok(Direction::Down),
|
"down" => Ok(Direction::Down),
|
||||||
_ => Err(ConfigError::InvalidBinding(format!("invalid direction {argument:?}"))),
|
_ => Err(ConfigError::Binding(format!("invalid direction {argument:?}"))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn parse_workspace(argument: &str) -> Result<u8, ConfigError> {
|
pub(super) fn parse_workspace(argument: &str) -> Result<u8, ConfigError> {
|
||||||
match argument.parse() {
|
match argument.parse() {
|
||||||
Ok(workspace) if workspace > 0 => Ok(workspace),
|
Ok(workspace) if workspace > 0 => Ok(workspace),
|
||||||
_ => Err(ConfigError::InvalidBinding(format!("invalid workspace {argument:?}"))),
|
_ => Err(ConfigError::Binding(format!("invalid workspace {argument:?}"))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,7 +454,7 @@ pub enum BindingAction {
|
|||||||
impl Binding {
|
impl Binding {
|
||||||
pub(crate) fn portal_trigger(chord: &str) -> Result<Self, ConfigError> {
|
pub(crate) fn portal_trigger(chord: &str) -> Result<Self, ConfigError> {
|
||||||
if chord.len() > 256 {
|
if chord.len() > 256 {
|
||||||
return Err(ConfigError::InvalidBinding("Shortcut is too long".into()));
|
return Err(ConfigError::Binding("Shortcut is too long".into()));
|
||||||
}
|
}
|
||||||
let parts = chord.split('+').collect::<Vec<_>>();
|
let parts = chord.split('+').collect::<Vec<_>>();
|
||||||
if parts
|
if parts
|
||||||
@@ -468,7 +463,7 @@ impl Binding {
|
|||||||
.count()
|
.count()
|
||||||
> 1
|
> 1
|
||||||
{
|
{
|
||||||
return Err(ConfigError::InvalidBinding("Duplicate NUM modifier".into()));
|
return Err(ConfigError::Binding("Duplicate NUM modifier".into()));
|
||||||
}
|
}
|
||||||
let chord = parts
|
let chord = parts
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -482,7 +477,7 @@ impl Binding {
|
|||||||
&& matches!(symbol, keysyms::KEY_Escape | keysyms::KEY_F1..=keysyms::KEY_F12))
|
&& matches!(symbol, keysyms::KEY_Escape | keysyms::KEY_F1..=keysyms::KEY_F12))
|
||||||
|| (keysyms::KEY_XF86Switch_VT_1..=keysyms::KEY_XF86Switch_VT_12).contains(&symbol)
|
|| (keysyms::KEY_XF86Switch_VT_1..=keysyms::KEY_XF86Switch_VT_12).contains(&symbol)
|
||||||
{
|
{
|
||||||
return Err(ConfigError::InvalidBinding("Reserved system shortcut".into()));
|
return Err(ConfigError::Binding("Reserved system shortcut".into()));
|
||||||
}
|
}
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
modifiers,
|
modifiers,
|
||||||
|
|||||||
@@ -3,26 +3,26 @@ use super::*;
|
|||||||
impl Config {
|
impl Config {
|
||||||
pub fn input_settings(&self) -> Result<InputSettings, ConfigError> {
|
pub fn input_settings(&self) -> Result<InputSettings, ConfigError> {
|
||||||
if self.input.xkb_layout.trim().is_empty() {
|
if self.input.xkb_layout.trim().is_empty() {
|
||||||
return Err(ConfigError::InvalidInputValue {
|
return Err(ConfigError::InputValue {
|
||||||
field: "xkb_layout",
|
field: "xkb_layout",
|
||||||
value: self.input.xkb_layout.clone(),
|
value: self.input.xkb_layout.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if self.input.repeat_rate <= 0 {
|
if self.input.repeat_rate <= 0 {
|
||||||
return Err(ConfigError::InvalidInputValue {
|
return Err(ConfigError::InputValue {
|
||||||
field: "repeat_rate",
|
field: "repeat_rate",
|
||||||
value: self.input.repeat_rate.to_string(),
|
value: self.input.repeat_rate.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if self.input.repeat_delay_ms < 0 {
|
if self.input.repeat_delay_ms < 0 {
|
||||||
return Err(ConfigError::InvalidInputValue {
|
return Err(ConfigError::InputValue {
|
||||||
field: "repeat_delay_ms",
|
field: "repeat_delay_ms",
|
||||||
value: self.input.repeat_delay_ms.to_string(),
|
value: self.input.repeat_delay_ms.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if !(16..=1000).contains(&self.input.touchpad.swipe_threshold) {
|
if !(16..=1000).contains(&self.input.touchpad.swipe_threshold) {
|
||||||
return Err(ConfigError::InvalidInputValue {
|
return Err(ConfigError::InputValue {
|
||||||
field: "touchpad.swipe_threshold",
|
field: "touchpad.swipe_threshold",
|
||||||
value: self.input.touchpad.swipe_threshold.to_string(),
|
value: self.input.touchpad.swipe_threshold.to_string(),
|
||||||
});
|
});
|
||||||
@@ -56,7 +56,7 @@ impl Config {
|
|||||||
let stiffness = positive_animation_value(self.animations.spring.stiffness, "spring.stiffness")?;
|
let stiffness = positive_animation_value(self.animations.spring.stiffness, "spring.stiffness")?;
|
||||||
let damping = self.animations.spring.damping;
|
let damping = self.animations.spring.damping;
|
||||||
if !damping.is_finite() || damping < 0.0 {
|
if !damping.is_finite() || damping < 0.0 {
|
||||||
return Err(ConfigError::InvalidAnimationValue {
|
return Err(ConfigError::AnimationValue {
|
||||||
field: "spring.damping",
|
field: "spring.damping",
|
||||||
value: damping,
|
value: damping,
|
||||||
});
|
});
|
||||||
@@ -92,7 +92,7 @@ pub(super) fn positive_animation_value(value: f64, field: &'static str) -> Resul
|
|||||||
if value.is_finite() && value > 0.0 {
|
if value.is_finite() && value > 0.0 {
|
||||||
Ok(value)
|
Ok(value)
|
||||||
} else {
|
} else {
|
||||||
Err(ConfigError::InvalidAnimationValue { field, value })
|
Err(ConfigError::AnimationValue { field, value })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ pub(super) fn nonnegative_layout_value(value: f64, field: &'static str) -> Resul
|
|||||||
if value.is_finite() && value >= 0.0 {
|
if value.is_finite() && value >= 0.0 {
|
||||||
Ok(value)
|
Ok(value)
|
||||||
} else {
|
} else {
|
||||||
Err(ConfigError::InvalidLayoutValue { field, value })
|
Err(ConfigError::LayoutValue { field, value })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ pub(super) fn parse_column_width(value: &ColumnWidthValue, field: &'static str)
|
|||||||
match value {
|
match value {
|
||||||
ColumnWidthValue::Proportion(value) if value.is_finite() && *value > 0.0 => Ok(ColumnWidth::Proportion(*value)),
|
ColumnWidthValue::Proportion(value) if value.is_finite() && *value > 0.0 => Ok(ColumnWidth::Proportion(*value)),
|
||||||
ColumnWidthValue::Named(value) if value.eq_ignore_ascii_case("full") => Ok(ColumnWidth::Full),
|
ColumnWidthValue::Named(value) if value.eq_ignore_ascii_case("full") => Ok(ColumnWidth::Full),
|
||||||
value => Err(ConfigError::InvalidColumnWidth {
|
value => Err(ConfigError::ColumnWidth {
|
||||||
field,
|
field,
|
||||||
value: match value {
|
value: match value {
|
||||||
ColumnWidthValue::Proportion(value) => value.to_string(),
|
ColumnWidthValue::Proportion(value) => value.to_string(),
|
||||||
|
|||||||
@@ -9,19 +9,13 @@ impl Config {
|
|||||||
.map(|profile| {
|
.map(|profile| {
|
||||||
let name = profile.name.trim();
|
let name = profile.name.trim();
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
return Err(ConfigError::InvalidOutputProfile(
|
return Err(ConfigError::OutputProfile("profile names cannot be empty".to_owned()));
|
||||||
"profile names cannot be empty".to_owned(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
if !names.insert(name.to_owned()) {
|
if !names.insert(name.to_owned()) {
|
||||||
return Err(ConfigError::InvalidOutputProfile(format!(
|
return Err(ConfigError::OutputProfile(format!("duplicate profile name {name:?}")));
|
||||||
"duplicate profile name {name:?}"
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
if profile.outputs.is_empty() {
|
if profile.outputs.is_empty() {
|
||||||
return Err(ConfigError::InvalidOutputProfile(format!(
|
return Err(ConfigError::OutputProfile(format!("profile {name:?} has no outputs")));
|
||||||
"profile {name:?} has no outputs"
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut matchers = HashSet::new();
|
let mut matchers = HashSet::new();
|
||||||
@@ -31,17 +25,17 @@ impl Config {
|
|||||||
.map(|output| {
|
.map(|output| {
|
||||||
let matcher = output.matcher.trim();
|
let matcher = output.matcher.trim();
|
||||||
if matcher.is_empty() {
|
if matcher.is_empty() {
|
||||||
return Err(ConfigError::InvalidOutputProfile(format!(
|
return Err(ConfigError::OutputProfile(format!(
|
||||||
"profile {name:?} contains an empty output matcher"
|
"profile {name:?} contains an empty output matcher"
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
if !matchers.insert(matcher.to_owned()) {
|
if !matchers.insert(matcher.to_owned()) {
|
||||||
return Err(ConfigError::InvalidOutputProfile(format!(
|
return Err(ConfigError::OutputProfile(format!(
|
||||||
"profile {name:?} repeats output matcher {matcher:?}"
|
"profile {name:?} repeats output matcher {matcher:?}"
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
if !output.scale.is_finite() || output.scale <= 0.0 {
|
if !output.scale.is_finite() || output.scale <= 0.0 {
|
||||||
return Err(ConfigError::InvalidOutputProfile(format!(
|
return Err(ConfigError::OutputProfile(format!(
|
||||||
"profile {name:?} output {matcher:?} has invalid scale {}",
|
"profile {name:?} output {matcher:?} has invalid scale {}",
|
||||||
output.scale
|
output.scale
|
||||||
)));
|
)));
|
||||||
@@ -56,7 +50,7 @@ impl Config {
|
|||||||
.as_deref()
|
.as_deref()
|
||||||
.map(parse_output_mode)
|
.map(parse_output_mode)
|
||||||
.transpose()
|
.transpose()
|
||||||
.map_err(ConfigError::InvalidOutputProfile)?,
|
.map_err(ConfigError::OutputProfile)?,
|
||||||
scale: output.scale,
|
scale: output.scale,
|
||||||
transform: output.transform,
|
transform: output.transform,
|
||||||
position: output.position,
|
position: output.position,
|
||||||
|
|||||||
+106
-103
@@ -215,10 +215,13 @@ impl InputCapture {
|
|||||||
}
|
}
|
||||||
for barrier in barriers {
|
for barrier in barriers {
|
||||||
let kind = edge(&barrier, &self.zones);
|
let kind = edge(&barrier, &self.zones);
|
||||||
if zone_set != self.zone_set || counts[&barrier.id] != 1 || kind.is_none() {
|
if zone_set == self.zone_set
|
||||||
failed.push(barrier.id);
|
&& counts[&barrier.id] == 1
|
||||||
|
&& let Some(kind) = kind
|
||||||
|
{
|
||||||
|
accepted.push((barrier, kind));
|
||||||
} else {
|
} else {
|
||||||
accepted.push((barrier, kind.unwrap()));
|
failed.push(barrier.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.sessions.get_mut(&id).unwrap().barriers = accepted;
|
self.sessions.get_mut(&id).unwrap().barriers = accepted;
|
||||||
@@ -418,6 +421,106 @@ impl InputCapture {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl crate::Ferese {
|
||||||
|
pub(crate) fn refresh_input_capture_zones(&mut self) {
|
||||||
|
if !self.input_capture.has_sessions() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let active = self.input_capture.active();
|
||||||
|
let zones = self
|
||||||
|
.space
|
||||||
|
.outputs()
|
||||||
|
.filter_map(|output| {
|
||||||
|
let rect = self.space.output_geometry(output)?;
|
||||||
|
Some(Zone {
|
||||||
|
width: u32::try_from(rect.size.w).ok()?,
|
||||||
|
height: u32::try_from(rect.size.h).ok()?,
|
||||||
|
x: rect.loc.x,
|
||||||
|
y: rect.loc.y,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
self.input_capture.update_zones(zones);
|
||||||
|
if active && !self.input_capture.active() {
|
||||||
|
self.restore_input_capture_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn restore_input_capture_focus(&mut self) {
|
||||||
|
self.input_capture.restore_focus = false;
|
||||||
|
self.cursor_redraw_pending = true;
|
||||||
|
if !self.session_lock.active {
|
||||||
|
self.restore_keyboard_focus();
|
||||||
|
if let Some(pointer) = self.seat.get_pointer() {
|
||||||
|
let location = pointer.current_location();
|
||||||
|
pointer.motion(
|
||||||
|
self,
|
||||||
|
self.surface_under(location),
|
||||||
|
&smithay::input::pointer::MotionEvent {
|
||||||
|
location,
|
||||||
|
serial: smithay::utils::SERIAL_COUNTER.next_serial(),
|
||||||
|
time: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
pointer.frame(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn capture_pointer_motion(
|
||||||
|
&mut self,
|
||||||
|
from: smithay::utils::Point<f64, smithay::utils::Logical>,
|
||||||
|
to: smithay::utils::Point<f64, smithay::utils::Logical>,
|
||||||
|
absolute: bool,
|
||||||
|
) -> bool {
|
||||||
|
let pointer = self.seat.get_pointer().expect("seat has pointer");
|
||||||
|
let active = self.input_capture.active();
|
||||||
|
let allowed = !self.session_lock.active
|
||||||
|
&& !self.overview.is_presenting()
|
||||||
|
&& !pointer.is_grabbed()
|
||||||
|
&& !self.swipe.active()
|
||||||
|
&& self.seat.get_keyboard().is_none_or(|keyboard| !keyboard.is_grabbed())
|
||||||
|
&& self.seat.get_touch().is_none_or(|touch| !touch.is_grabbed())
|
||||||
|
&& !self.seat.keyboard_shortcuts_inhibited();
|
||||||
|
if !self.input_capture.motion((from.x, from.y), (to.x, to.y), allowed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let delta = if absolute {
|
||||||
|
self.input_capture.absolute_delta((to.x, to.y))
|
||||||
|
} else {
|
||||||
|
(to.x - from.x, to.y - from.y)
|
||||||
|
};
|
||||||
|
if !active {
|
||||||
|
self.cursor_redraw_pending = true;
|
||||||
|
pointer.motion(
|
||||||
|
self,
|
||||||
|
None,
|
||||||
|
&smithay::input::pointer::MotionEvent {
|
||||||
|
location: from,
|
||||||
|
serial: smithay::utils::SERIAL_COUNTER.next_serial(),
|
||||||
|
time: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
pointer.frame(self);
|
||||||
|
if self.input_capture.captures(1) {
|
||||||
|
let keyboard = self.seat.get_keyboard().expect("seat has keyboard");
|
||||||
|
let keys = keyboard
|
||||||
|
.pressed_keys()
|
||||||
|
.iter()
|
||||||
|
.filter(|key| !self.intercepted_keys.contains(key))
|
||||||
|
.map(|key| key.raw().saturating_sub(8))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
keyboard.set_focus(self, None, smithay::utils::SERIAL_COUNTER.next_serial());
|
||||||
|
self.input_capture.push(json!({"type":"keys", "keys":keys}));
|
||||||
|
}
|
||||||
|
} else if self.input_capture.captures(2) {
|
||||||
|
self.input_capture
|
||||||
|
.push(json!({"type":"motion", "x":delta.0, "y":delta.1}));
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -556,103 +659,3 @@ mod tests {
|
|||||||
assert!(capture.sessions[&id].barriers.is_empty());
|
assert!(capture.sessions[&id].barriers.is_empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl crate::Ferese {
|
|
||||||
pub(crate) fn refresh_input_capture_zones(&mut self) {
|
|
||||||
if !self.input_capture.has_sessions() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let active = self.input_capture.active();
|
|
||||||
let zones = self
|
|
||||||
.space
|
|
||||||
.outputs()
|
|
||||||
.filter_map(|output| {
|
|
||||||
let rect = self.space.output_geometry(output)?;
|
|
||||||
Some(Zone {
|
|
||||||
width: u32::try_from(rect.size.w).ok()?,
|
|
||||||
height: u32::try_from(rect.size.h).ok()?,
|
|
||||||
x: rect.loc.x,
|
|
||||||
y: rect.loc.y,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
self.input_capture.update_zones(zones);
|
|
||||||
if active && !self.input_capture.active() {
|
|
||||||
self.restore_input_capture_focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn restore_input_capture_focus(&mut self) {
|
|
||||||
self.input_capture.restore_focus = false;
|
|
||||||
self.cursor_redraw_pending = true;
|
|
||||||
if !self.session_lock.active {
|
|
||||||
self.restore_keyboard_focus();
|
|
||||||
if let Some(pointer) = self.seat.get_pointer() {
|
|
||||||
let location = pointer.current_location();
|
|
||||||
pointer.motion(
|
|
||||||
self,
|
|
||||||
self.surface_under(location),
|
|
||||||
&smithay::input::pointer::MotionEvent {
|
|
||||||
location,
|
|
||||||
serial: smithay::utils::SERIAL_COUNTER.next_serial(),
|
|
||||||
time: 0,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
pointer.frame(self);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn capture_pointer_motion(
|
|
||||||
&mut self,
|
|
||||||
from: smithay::utils::Point<f64, smithay::utils::Logical>,
|
|
||||||
to: smithay::utils::Point<f64, smithay::utils::Logical>,
|
|
||||||
absolute: bool,
|
|
||||||
) -> bool {
|
|
||||||
let pointer = self.seat.get_pointer().expect("seat has pointer");
|
|
||||||
let active = self.input_capture.active();
|
|
||||||
let allowed = !self.session_lock.active
|
|
||||||
&& !self.overview.is_presenting()
|
|
||||||
&& !pointer.is_grabbed()
|
|
||||||
&& !self.swipe.active()
|
|
||||||
&& self.seat.get_keyboard().is_none_or(|keyboard| !keyboard.is_grabbed())
|
|
||||||
&& self.seat.get_touch().is_none_or(|touch| !touch.is_grabbed())
|
|
||||||
&& !self.seat.keyboard_shortcuts_inhibited();
|
|
||||||
if !self.input_capture.motion((from.x, from.y), (to.x, to.y), allowed) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let delta = if absolute {
|
|
||||||
self.input_capture.absolute_delta((to.x, to.y))
|
|
||||||
} else {
|
|
||||||
(to.x - from.x, to.y - from.y)
|
|
||||||
};
|
|
||||||
if !active {
|
|
||||||
self.cursor_redraw_pending = true;
|
|
||||||
pointer.motion(
|
|
||||||
self,
|
|
||||||
None,
|
|
||||||
&smithay::input::pointer::MotionEvent {
|
|
||||||
location: from,
|
|
||||||
serial: smithay::utils::SERIAL_COUNTER.next_serial(),
|
|
||||||
time: 0,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
pointer.frame(self);
|
|
||||||
if self.input_capture.captures(1) {
|
|
||||||
let keyboard = self.seat.get_keyboard().expect("seat has keyboard");
|
|
||||||
let keys = keyboard
|
|
||||||
.pressed_keys()
|
|
||||||
.iter()
|
|
||||||
.filter(|key| !self.intercepted_keys.contains(key))
|
|
||||||
.map(|key| key.raw().saturating_sub(8))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
keyboard.set_focus(self, None, smithay::utils::SERIAL_COUNTER.next_serial());
|
|
||||||
self.input_capture.push(json!({"type":"keys", "keys":keys}));
|
|
||||||
}
|
|
||||||
} else if self.input_capture.captures(2) {
|
|
||||||
self.input_capture
|
|
||||||
.push(json!({"type":"motion", "x":delta.0, "y":delta.1}));
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -104,11 +104,10 @@ pub(crate) fn capture_window_frame(
|
|||||||
pixel.swap(0, 2);
|
pixel.swap(0, 2);
|
||||||
let alpha = pixel[3] as u16;
|
let alpha = pixel[3] as u16;
|
||||||
for channel in &mut pixel[..3] {
|
for channel in &mut pixel[..3] {
|
||||||
*channel = if alpha == 0 {
|
*channel = (*channel as u16 * 255 + alpha / 2)
|
||||||
0
|
.checked_div(alpha)
|
||||||
} else {
|
.unwrap_or(0)
|
||||||
((*channel as u16 * 255 + alpha / 2) / alpha).min(255) as u8
|
.min(255) as u8;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(crate::handlers::screenshot::CaptureBuffer {
|
Ok(crate::handlers::screenshot::CaptureBuffer {
|
||||||
|
|||||||
@@ -37,7 +37,19 @@ pub(super) fn append_material_surface(
|
|||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter_map(|(index, (rect, radius))| {
|
.filter_map(|(index, (rect, radius))| {
|
||||||
material_element(state, renderer, output, surface, *rect, *radius, index, geometry, 1.0)
|
material_element(
|
||||||
|
state,
|
||||||
|
renderer,
|
||||||
|
output,
|
||||||
|
surface,
|
||||||
|
MaterialSurface {
|
||||||
|
geometry: *rect,
|
||||||
|
radius: *radius,
|
||||||
|
index,
|
||||||
|
capture_geometry: geometry,
|
||||||
|
alpha: 1.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
if let Some(buffers) = state.material_buffers.get_mut(surface) {
|
if let Some(buffers) = state.material_buffers.get_mut(surface) {
|
||||||
@@ -89,17 +101,28 @@ pub(super) fn append_material_surface(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) struct MaterialSurface {
|
||||||
|
pub geometry: Rectangle<i32, Logical>,
|
||||||
|
pub radius: f32,
|
||||||
|
pub index: usize,
|
||||||
|
pub capture_geometry: Rectangle<i32, Logical>,
|
||||||
|
pub alpha: f32,
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn material_element(
|
pub(super) fn material_element(
|
||||||
state: &mut Ferese,
|
state: &mut Ferese,
|
||||||
renderer: &mut GlesRenderer,
|
renderer: &mut GlesRenderer,
|
||||||
output: &Output,
|
output: &Output,
|
||||||
surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface,
|
surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface,
|
||||||
geometry: Rectangle<i32, Logical>,
|
surface_geometry: MaterialSurface,
|
||||||
radius: f32,
|
|
||||||
index: usize,
|
|
||||||
capture_geometry: Rectangle<i32, Logical>,
|
|
||||||
alpha: f32,
|
|
||||||
) -> Option<(AnimatedWindowRenderElement, AnimatedWindowRenderElement)> {
|
) -> Option<(AnimatedWindowRenderElement, AnimatedWindowRenderElement)> {
|
||||||
|
let MaterialSurface {
|
||||||
|
geometry,
|
||||||
|
radius,
|
||||||
|
index,
|
||||||
|
capture_geometry,
|
||||||
|
alpha,
|
||||||
|
} = surface_geometry;
|
||||||
let (role, generation) = crate::effects::surface_role(surface)?;
|
let (role, generation) = crate::effects::surface_role(surface)?;
|
||||||
let material = crate::effects::resolve_material(
|
let material = crate::effects::resolve_material(
|
||||||
role,
|
role,
|
||||||
|
|||||||
@@ -527,7 +527,7 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
let size = (32, 32).into();
|
let size = (32, 32).into();
|
||||||
let mut pixels = vec![128u8; 32 * 32 * 4];
|
let mut pixels = vec![128u8; 32 * 32 * 4];
|
||||||
for pixel in pixels.chunks_exact_mut(4) {
|
for pixel in pixels.as_chunks_mut::<4>().0 {
|
||||||
pixel[3] = 255;
|
pixel[3] = 255;
|
||||||
}
|
}
|
||||||
let blurred = renderer.import_memory(&pixels, Fourcc::Abgr8888, size, false).unwrap();
|
let blurred = renderer.import_memory(&pixels, Fourcc::Abgr8888, size, false).unwrap();
|
||||||
|
|||||||
@@ -306,11 +306,13 @@ pub(crate) fn output_elements(
|
|||||||
renderer,
|
renderer,
|
||||||
output,
|
output,
|
||||||
surface,
|
surface,
|
||||||
constrain,
|
MaterialSurface {
|
||||||
window_radius as f32,
|
geometry: constrain,
|
||||||
0,
|
radius: window_radius as f32,
|
||||||
constrain,
|
index: 0,
|
||||||
close_alpha,
|
capture_geometry: constrain,
|
||||||
|
alpha: close_alpha,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
elements.push(background);
|
elements.push(background);
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ impl Default for IdleSettings {
|
|||||||
impl IdleSettings {
|
impl IdleSettings {
|
||||||
pub(crate) fn validate(self) -> Result<Self, crate::config::ConfigError> {
|
pub(crate) fn validate(self) -> Result<Self, crate::config::ConfigError> {
|
||||||
if self.dim_after_seconds > 86400 || self.sleep_after_seconds > 86400 {
|
if self.dim_after_seconds > 86400 || self.sleep_after_seconds > 86400 {
|
||||||
return Err(crate::config::ConfigError::InvalidInputValue {
|
return Err(crate::config::ConfigError::InputValue {
|
||||||
field: "lock_screen.idle_delay",
|
field: "lock_screen.idle_delay",
|
||||||
value: "must be between 0 and 86400 seconds".into(),
|
value: "must be between 0 and 86400 seconds".into(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1248,6 +1248,53 @@ impl ResolvedTheme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub type ImportedFamily = (crate::families::Family, (Option<Tokens>, Option<Tokens>), Vec<String>);
|
||||||
|
|
||||||
|
pub fn import_family(id: &str, source: &str) -> Result<ImportedFamily, String> {
|
||||||
|
let document = Document::parse(source).map_err(|e| e.to_string())?;
|
||||||
|
let root = document.get("theme").unwrap_or(document.value());
|
||||||
|
let name = root.get("name").and_then(Value::as_str).unwrap_or(id).to_owned();
|
||||||
|
let mut variants = [None, None];
|
||||||
|
let mut warnings = vec![];
|
||||||
|
for (index, (key, appearance)) in [("light", Appearance::Light), ("dark", Appearance::Dark)]
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
if let Some(layer) = root.get(key) {
|
||||||
|
let base = preset(
|
||||||
|
if appearance == Appearance::Light {
|
||||||
|
"ferese-blue-light"
|
||||||
|
} else {
|
||||||
|
"ferese-blue"
|
||||||
|
},
|
||||||
|
appearance,
|
||||||
|
)?;
|
||||||
|
let mut value = serde_json::to_value(base).map_err(|e| e.to_string())?;
|
||||||
|
merge(&mut value, layer, "", &mut warnings, false)?;
|
||||||
|
let tokens: Tokens = serde_json::from_value(value).map_err(|e| e.to_string())?;
|
||||||
|
validate(&tokens)?;
|
||||||
|
warnings.extend(authored_warnings(&tokens));
|
||||||
|
variants[index] = Some(tokens);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if variants.iter().all(Option::is_none) {
|
||||||
|
return Err("A theme file must declare a light and/or dark section; partial palettes are allowed.".into());
|
||||||
|
}
|
||||||
|
for key in root.as_object().ok_or("Theme file must be a section")?.keys() {
|
||||||
|
if !matches!(key.as_str(), "name" | "light" | "dark") {
|
||||||
|
warnings.push(format!("Unknown theme token: {key}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let [light, dark] = variants;
|
||||||
|
let family = crate::families::Family {
|
||||||
|
id: id.into(),
|
||||||
|
name,
|
||||||
|
light: light.as_ref().map(crate::families::Palette::from),
|
||||||
|
dark: dark.as_ref().map(crate::families::Palette::from),
|
||||||
|
};
|
||||||
|
Ok((family, (light, dark), warnings))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -1697,51 +1744,3 @@ mod tests {
|
|||||||
assert_eq!(ResolvedTheme::default().transition(&light, 0.), light);
|
assert_eq!(ResolvedTheme::default().transition(&light, 0.), light);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn import_family(
|
|
||||||
id: &str,
|
|
||||||
source: &str,
|
|
||||||
) -> Result<(crate::families::Family, (Option<Tokens>, Option<Tokens>), Vec<String>), String> {
|
|
||||||
let document = Document::parse(source).map_err(|e| e.to_string())?;
|
|
||||||
let root = document.get("theme").unwrap_or(document.value());
|
|
||||||
let name = root.get("name").and_then(Value::as_str).unwrap_or(id).to_owned();
|
|
||||||
let mut variants = [None, None];
|
|
||||||
let mut warnings = vec![];
|
|
||||||
for (index, (key, appearance)) in [("light", Appearance::Light), ("dark", Appearance::Dark)]
|
|
||||||
.into_iter()
|
|
||||||
.enumerate()
|
|
||||||
{
|
|
||||||
if let Some(layer) = root.get(key) {
|
|
||||||
let base = preset(
|
|
||||||
if appearance == Appearance::Light {
|
|
||||||
"ferese-blue-light"
|
|
||||||
} else {
|
|
||||||
"ferese-blue"
|
|
||||||
},
|
|
||||||
appearance,
|
|
||||||
)?;
|
|
||||||
let mut value = serde_json::to_value(base).map_err(|e| e.to_string())?;
|
|
||||||
merge(&mut value, layer, "", &mut warnings, false)?;
|
|
||||||
let tokens: Tokens = serde_json::from_value(value).map_err(|e| e.to_string())?;
|
|
||||||
validate(&tokens)?;
|
|
||||||
warnings.extend(authored_warnings(&tokens));
|
|
||||||
variants[index] = Some(tokens);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if variants.iter().all(Option::is_none) {
|
|
||||||
return Err("A theme file must declare a light and/or dark section; partial palettes are allowed.".into());
|
|
||||||
}
|
|
||||||
for key in root.as_object().ok_or("Theme file must be a section")?.keys() {
|
|
||||||
if !matches!(key.as_str(), "name" | "light" | "dark") {
|
|
||||||
warnings.push(format!("Unknown theme token: {key}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let [light, dark] = variants;
|
|
||||||
let family = crate::families::Family {
|
|
||||||
id: id.into(),
|
|
||||||
name,
|
|
||||||
light: light.as_ref().map(crate::families::Palette::from),
|
|
||||||
dark: dark.as_ref().map(crate::families::Palette::from),
|
|
||||||
};
|
|
||||||
Ok((family, (light, dark), warnings))
|
|
||||||
}
|
|
||||||
@@ -6,17 +6,29 @@ use ferese_config::families::{Family, Palette};
|
|||||||
use ferese_config::theme::Appearance;
|
use ferese_config::theme::Appearance;
|
||||||
use iced_accessibility::{A11yTree, accesskit};
|
use iced_accessibility::{A11yTree, accesskit};
|
||||||
|
|
||||||
|
pub struct TileOptions {
|
||||||
|
pub variant: Option<Appearance>,
|
||||||
|
pub active: Option<Appearance>,
|
||||||
|
pub selected: bool,
|
||||||
|
pub palette: crate::Palette,
|
||||||
|
pub font: cosmic::font::Font,
|
||||||
|
pub id: Id,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn tile<M: Clone + 'static>(
|
pub fn tile<M: Clone + 'static>(
|
||||||
family: &Family,
|
family: &Family,
|
||||||
variant: Option<Appearance>,
|
options: TileOptions,
|
||||||
active: Option<Appearance>,
|
|
||||||
selected: bool,
|
|
||||||
palette: crate::Palette,
|
|
||||||
font: cosmic::font::Font,
|
|
||||||
id: Id,
|
|
||||||
on_select: M,
|
on_select: M,
|
||||||
navigate: impl Fn(&keyboard::key::Named) -> Option<M> + 'static,
|
navigate: impl Fn(&keyboard::key::Named) -> Option<M> + 'static,
|
||||||
) -> Element<'static, M> {
|
) -> Element<'static, M> {
|
||||||
|
let TileOptions {
|
||||||
|
variant,
|
||||||
|
active,
|
||||||
|
selected,
|
||||||
|
palette,
|
||||||
|
font,
|
||||||
|
id,
|
||||||
|
} = options;
|
||||||
use cosmic::widget::{button, column, row};
|
use cosmic::widget::{button, column, row};
|
||||||
let mut caption = row([])
|
let mut caption = row([])
|
||||||
.spacing(6)
|
.spacing(6)
|
||||||
@@ -225,13 +237,15 @@ impl cosmic::iced::advanced::widget::operation::Focusable for Focus {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Navigation<'a, M> = Box<dyn Fn(&keyboard::key::Named) -> Option<M> + 'a>;
|
||||||
|
|
||||||
pub struct Radio<'a, M> {
|
pub struct Radio<'a, M> {
|
||||||
content: Element<'a, M>,
|
content: Element<'a, M>,
|
||||||
id: Id,
|
id: Id,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
group: bool,
|
group: bool,
|
||||||
label: String,
|
label: String,
|
||||||
navigate: Option<Box<dyn Fn(&keyboard::key::Named) -> Option<M> + 'a>>,
|
navigate: Option<Navigation<'a, M>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn radio<'a, M: Clone + 'a>(
|
pub fn radio<'a, M: Clone + 'a>(
|
||||||
|
|||||||
@@ -120,8 +120,10 @@ mod tests {
|
|||||||
ferese_config::theme::Appearance::Dark,
|
ferese_config::theme::Appearance::Dark,
|
||||||
] {
|
] {
|
||||||
for high_contrast in [false, true] {
|
for high_contrast in [false, true] {
|
||||||
let mut resolved = ferese_config::theme::ResolvedTheme::default();
|
let mut resolved = ferese_config::theme::ResolvedTheme {
|
||||||
resolved.appearance = appearance;
|
appearance,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
resolved.accessibility.increase_contrast = high_contrast;
|
resolved.accessibility.increase_contrast = high_contrast;
|
||||||
let palette = Palette::from_resolved(&resolved);
|
let palette = Palette::from_resolved(&resolved);
|
||||||
assert_eq!(palette.high_contrast, high_contrast);
|
assert_eq!(palette.high_contrast, high_contrast);
|
||||||
@@ -133,9 +135,11 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn every_preset_builds_an_opaque_control_palette_over_translucent_materials() {
|
fn every_preset_builds_an_opaque_control_palette_over_translucent_materials() {
|
||||||
for preset in crate::PRESETS {
|
for preset in crate::PRESETS {
|
||||||
let mut resolved = ferese_config::theme::ResolvedTheme::default();
|
let mut resolved = ferese_config::theme::ResolvedTheme {
|
||||||
resolved.appearance = preset.appearance;
|
appearance: preset.appearance,
|
||||||
resolved.tokens = ferese_config::theme::preset(preset.id, preset.appearance).unwrap();
|
tokens: ferese_config::theme::preset(preset.id, preset.appearance).unwrap(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
resolved.tokens.material.style = "translucent".into();
|
resolved.tokens.material.style = "translucent".into();
|
||||||
resolved.tokens.material.opacity = 0.25;
|
resolved.tokens.material.opacity = 0.25;
|
||||||
resolved.tokens.geometry.shell_radius = 0.;
|
resolved.tokens.geometry.shell_radius = 0.;
|
||||||
|
|||||||
+18
-4
@@ -393,12 +393,14 @@ impl Backend {
|
|||||||
let sharing = start_streams(
|
let sharing = start_streams(
|
||||||
&app_id,
|
&app_id,
|
||||||
&parent_window,
|
&parent_window,
|
||||||
|
StreamOptions {
|
||||||
multiple,
|
multiple,
|
||||||
cursor,
|
cursor,
|
||||||
source_types,
|
source_types,
|
||||||
persist_mode,
|
requested_persistence: persist_mode,
|
||||||
restore,
|
restore,
|
||||||
bar_controlled,
|
bar_controlled,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
let result = tokio::select! {
|
let result = tokio::select! {
|
||||||
_ = cancel.wait() => Err(StartError::Cancelled),
|
_ = cancel.wait() => Err(StartError::Cancelled),
|
||||||
@@ -534,16 +536,28 @@ async fn picker(prompt: &Prompt) -> Result<Child, String> {
|
|||||||
Ok(child)
|
Ok(child)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn start_streams(
|
struct StreamOptions {
|
||||||
app: &str,
|
|
||||||
parent: &str,
|
|
||||||
multiple: bool,
|
multiple: bool,
|
||||||
cursor: bool,
|
cursor: bool,
|
||||||
source_types: u32,
|
source_types: u32,
|
||||||
requested_persistence: u32,
|
requested_persistence: u32,
|
||||||
restore: Option<crate::restore::Restore>,
|
restore: Option<crate::restore::Restore>,
|
||||||
bar_controlled: bool,
|
bar_controlled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn start_streams(
|
||||||
|
app: &str,
|
||||||
|
parent: &str,
|
||||||
|
options: StreamOptions,
|
||||||
) -> Result<(Vec<Child>, Vec<(u32, Options)>, Options), StartError> {
|
) -> Result<(Vec<Child>, Vec<(u32, Options)>, Options), StartError> {
|
||||||
|
let StreamOptions {
|
||||||
|
multiple,
|
||||||
|
cursor,
|
||||||
|
source_types,
|
||||||
|
requested_persistence,
|
||||||
|
restore,
|
||||||
|
bar_controlled,
|
||||||
|
} = options;
|
||||||
let mut discovery = tokio::task::spawn_blocking(|| Capture::connect(&AtomicBool::new(false)))
|
let mut discovery = tokio::task::spawn_blocking(|| Capture::connect(&AtomicBool::new(false)))
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())??;
|
.map_err(|e| e.to_string())??;
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ pub(crate) struct ShortcutField {
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
enum Message {
|
enum Message {
|
||||||
ThemeChanged(ferese_config::theme::Snapshot),
|
ThemeChanged(Box<ferese_config::theme::Snapshot>),
|
||||||
Opened(cosmic::iced::window::Id),
|
Opened(cosmic::iced::window::Id),
|
||||||
Attached(Result<(Option<Arc<crate::parent::Parent>>, Result<ModalMaterial, String>), String>),
|
Attached(crate::parent::Attachment),
|
||||||
Accept,
|
Accept,
|
||||||
Cancel,
|
Cancel,
|
||||||
Shortcut(usize, String),
|
Shortcut(usize, String),
|
||||||
@@ -110,7 +110,7 @@ impl cosmic::Application for Consent {
|
|||||||
|
|
||||||
fn subscription(&self) -> cosmic::iced::Subscription<Message> {
|
fn subscription(&self) -> cosmic::iced::Subscription<Message> {
|
||||||
cosmic::iced::Subscription::batch([
|
cosmic::iced::Subscription::batch([
|
||||||
ferese_theme::service::subscription().map(Message::ThemeChanged),
|
ferese_theme::service::subscription().map(|snapshot| Message::ThemeChanged(Box::new(snapshot))),
|
||||||
cosmic::iced::event::listen_with(|event, _, id| match event {
|
cosmic::iced::event::listen_with(|event, _, id| match event {
|
||||||
cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => Some(Message::Opened(id)),
|
cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => Some(Message::Opened(id)),
|
||||||
cosmic::iced::Event::Keyboard(cosmic::iced::keyboard::Event::KeyPressed {
|
cosmic::iced::Event::Keyboard(cosmic::iced::keyboard::Event::KeyPressed {
|
||||||
|
|||||||
@@ -21,8 +21,10 @@ const PATH: &str = "/org/freedesktop/portal/desktop";
|
|||||||
const MAX_IMAGE_BYTES: u64 = 128 * 1024 * 1024;
|
const MAX_IMAGE_BYTES: u64 = 128 * 1024 * 1024;
|
||||||
type Reply = (u32, Options);
|
type Reply = (u32, Options);
|
||||||
|
|
||||||
|
type RequestMap = HashMap<String, (String, Arc<Cancel>)>;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct Requests(Arc<Mutex<HashMap<String, (String, Arc<Cancel>)>>>, usize);
|
pub(crate) struct Requests(Arc<Mutex<RequestMap>>, usize);
|
||||||
|
|
||||||
impl Default for Requests {
|
impl Default for Requests {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
@@ -141,6 +143,10 @@ pub(crate) struct Wallpaper(pub(crate) Requests);
|
|||||||
#[zbus::interface(name = "org.freedesktop.impl.portal.Wallpaper")]
|
#[zbus::interface(name = "org.freedesktop.impl.portal.Wallpaper")]
|
||||||
impl Wallpaper {
|
impl Wallpaper {
|
||||||
#[zbus(name = "SetWallpaperURI")]
|
#[zbus(name = "SetWallpaperURI")]
|
||||||
|
#[allow(
|
||||||
|
clippy::too_many_arguments,
|
||||||
|
reason = "Signature is defined by the portal D-Bus interface"
|
||||||
|
)]
|
||||||
async fn set_wallpaper_uri(
|
async fn set_wallpaper_uri(
|
||||||
&self,
|
&self,
|
||||||
handle: OwnedObjectPath,
|
handle: OwnedObjectPath,
|
||||||
|
|||||||
@@ -258,6 +258,10 @@ async fn login_inhibitor(what: &str, app: &str, reason: &str) -> Result<Option<O
|
|||||||
|
|
||||||
#[zbus::interface(name = "org.freedesktop.impl.portal.Inhibit")]
|
#[zbus::interface(name = "org.freedesktop.impl.portal.Inhibit")]
|
||||||
impl Inhibit {
|
impl Inhibit {
|
||||||
|
#[allow(
|
||||||
|
clippy::too_many_arguments,
|
||||||
|
reason = "Signature is defined by the portal D-Bus interface"
|
||||||
|
)]
|
||||||
async fn inhibit(
|
async fn inhibit(
|
||||||
&self,
|
&self,
|
||||||
handle: OwnedObjectPath,
|
handle: OwnedObjectPath,
|
||||||
|
|||||||
@@ -1,3 +1,11 @@
|
|||||||
|
pub(crate) type Attachment = Result<
|
||||||
|
(
|
||||||
|
Option<std::sync::Arc<Parent>>,
|
||||||
|
Result<ferese_theme::material::ModalMaterial, String>,
|
||||||
|
),
|
||||||
|
String,
|
||||||
|
>;
|
||||||
|
|
||||||
use wayland_client::globals::{GlobalListContents, registry_queue_init};
|
use wayland_client::globals::{GlobalListContents, registry_queue_init};
|
||||||
use wayland_client::protocol::{wl_registry, wl_surface};
|
use wayland_client::protocol::{wl_registry, wl_surface};
|
||||||
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, delegate_noop};
|
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, delegate_noop};
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ impl Usb {
|
|||||||
1
|
1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(
|
||||||
|
clippy::too_many_arguments,
|
||||||
|
reason = "Signature is defined by the portal D-Bus interface"
|
||||||
|
)]
|
||||||
async fn acquire_devices(
|
async fn acquire_devices(
|
||||||
&self,
|
&self,
|
||||||
handle: OwnedObjectPath,
|
handle: OwnedObjectPath,
|
||||||
|
|||||||
+3
-11
@@ -52,17 +52,9 @@ pub(crate) struct Selection {
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
enum Message {
|
enum Message {
|
||||||
ThemeChanged(ferese_config::theme::Snapshot),
|
ThemeChanged(Box<ferese_config::theme::Snapshot>),
|
||||||
WindowOpened(cosmic::iced::window::Id),
|
WindowOpened(cosmic::iced::window::Id),
|
||||||
Attached(
|
Attached(crate::parent::Attachment),
|
||||||
Result<
|
|
||||||
(
|
|
||||||
Option<std::sync::Arc<crate::parent::Parent>>,
|
|
||||||
Result<ferese_theme::material::ModalMaterial, String>,
|
|
||||||
),
|
|
||||||
String,
|
|
||||||
>,
|
|
||||||
),
|
|
||||||
Select(usize),
|
Select(usize),
|
||||||
Remember,
|
Remember,
|
||||||
Share,
|
Share,
|
||||||
@@ -161,7 +153,7 @@ impl cosmic::Application for Picker {
|
|||||||
|
|
||||||
fn subscription(&self) -> cosmic::iced::Subscription<Message> {
|
fn subscription(&self) -> cosmic::iced::Subscription<Message> {
|
||||||
cosmic::iced::Subscription::batch([
|
cosmic::iced::Subscription::batch([
|
||||||
ferese_theme::service::subscription().map(Message::ThemeChanged),
|
ferese_theme::service::subscription().map(|snapshot| Message::ThemeChanged(Box::new(snapshot))),
|
||||||
cosmic::iced::event::listen_with(|event, _, id| match event {
|
cosmic::iced::event::listen_with(|event, _, id| match event {
|
||||||
cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => {
|
cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => {
|
||||||
Some(Message::WindowOpened(id))
|
Some(Message::WindowOpened(id))
|
||||||
|
|||||||
@@ -282,8 +282,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reports_ferese_theme_and_reduced_motion() {
|
fn reports_ferese_theme_and_reduced_motion() {
|
||||||
let mut resolved = ResolvedTheme::default();
|
let mut resolved = ResolvedTheme {
|
||||||
resolved.appearance = ThemeAppearance::Light;
|
appearance: ThemeAppearance::Light,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
resolved.tokens.colors.accent = "#ff8000".into();
|
resolved.tokens.colors.accent = "#ff8000".into();
|
||||||
resolved.reduced_motion = true;
|
resolved.reduced_motion = true;
|
||||||
resolved.accessibility.increase_contrast = true;
|
resolved.accessibility.increase_contrast = true;
|
||||||
|
|||||||
@@ -310,6 +310,10 @@ impl GlobalShortcuts {
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(
|
||||||
|
clippy::too_many_arguments,
|
||||||
|
reason = "Signature is defined by the portal D-Bus interface"
|
||||||
|
)]
|
||||||
async fn bind_shortcuts(
|
async fn bind_shortcuts(
|
||||||
&self,
|
&self,
|
||||||
handle: OwnedObjectPath,
|
handle: OwnedObjectPath,
|
||||||
|
|||||||
@@ -169,6 +169,35 @@ fn account_picture(user: &str) -> Option<image::Handle> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn load_wallpaper(path: &std::path::Path, blur: f32) -> Result<image::Handle, String> {
|
||||||
|
use ::image::ImageDecoder;
|
||||||
|
let mut reader = ::image::ImageReader::open(path)
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.with_guessed_format()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let mut limits = ::image::Limits::default();
|
||||||
|
limits.max_alloc = Some(256 * 1024 * 1024);
|
||||||
|
reader.limits(limits);
|
||||||
|
let decoder = reader.into_decoder().map_err(|e| e.to_string())?;
|
||||||
|
let (width, height) = decoder.dimensions();
|
||||||
|
if u64::from(width) * u64::from(height) * 4 > 256 * 1024 * 1024 {
|
||||||
|
return Err("Lock wallpaper exceeds the decode limit".into());
|
||||||
|
}
|
||||||
|
let pixels = ::image::DynamicImage::from_decoder(decoder)
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.into_rgba8();
|
||||||
|
let pixels = if blur > 0. {
|
||||||
|
::image::imageops::blur(&::image::imageops::thumbnail(&pixels, 640, 640), blur)
|
||||||
|
} else {
|
||||||
|
pixels
|
||||||
|
};
|
||||||
|
Ok(image::Handle::from_rgba(
|
||||||
|
pixels.width(),
|
||||||
|
pixels.height(),
|
||||||
|
pixels.into_raw(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -215,39 +244,10 @@ mod tests {
|
|||||||
appearance.accent,
|
appearance.accent,
|
||||||
ferese_theme::parse_color(&theme.tokens.colors.accent).unwrap()
|
ferese_theme::parse_color(&theme.tokens.colors.accent).unwrap()
|
||||||
);
|
);
|
||||||
assert!(ferese_theme::contrast(appearance.accent.into(), appearance.on_accent.into()) >= 4.5);
|
assert!(ferese_theme::contrast(appearance.accent, appearance.on_accent) >= 4.5);
|
||||||
assert!(!appearance.show_clock && !appearance.show_date);
|
assert!(!appearance.show_clock && !appearance.show_date);
|
||||||
assert_eq!(appearance.clock_format(), "%I:%M %p");
|
assert_eq!(appearance.clock_format(), "%I:%M %p");
|
||||||
assert_eq!(appearance.dim, 0.7);
|
assert_eq!(appearance.dim, 0.7);
|
||||||
assert_eq!(appearance.theme().cosmic().corner_radii.radius_m, [0.; 4]);
|
assert_eq!(appearance.theme().cosmic().corner_radii.radius_m, [0.; 4]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_wallpaper(path: &std::path::Path, blur: f32) -> Result<image::Handle, String> {
|
|
||||||
use ::image::ImageDecoder;
|
|
||||||
let mut reader = ::image::ImageReader::open(path)
|
|
||||||
.map_err(|e| e.to_string())?
|
|
||||||
.with_guessed_format()
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
let mut limits = ::image::Limits::default();
|
|
||||||
limits.max_alloc = Some(256 * 1024 * 1024);
|
|
||||||
reader.limits(limits);
|
|
||||||
let decoder = reader.into_decoder().map_err(|e| e.to_string())?;
|
|
||||||
let (width, height) = decoder.dimensions();
|
|
||||||
if u64::from(width) * u64::from(height) * 4 > 256 * 1024 * 1024 {
|
|
||||||
return Err("Lock wallpaper exceeds the decode limit".into());
|
|
||||||
}
|
|
||||||
let pixels = ::image::DynamicImage::from_decoder(decoder)
|
|
||||||
.map_err(|e| e.to_string())?
|
|
||||||
.into_rgba8();
|
|
||||||
let pixels = if blur > 0. {
|
|
||||||
::image::imageops::blur(&::image::imageops::thumbnail(&pixels, 640, 640), blur)
|
|
||||||
} else {
|
|
||||||
pixels
|
|
||||||
};
|
|
||||||
Ok(image::Handle::from_rgba(
|
|
||||||
pixels.width(),
|
|
||||||
pixels.height(),
|
|
||||||
pixels.into_raw(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
@@ -82,7 +82,7 @@ fn main() -> iced::Result {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
enum Message {
|
enum Message {
|
||||||
ThemeChanged(ferese_config::theme::Snapshot),
|
ThemeChanged(Box<ferese_config::theme::Snapshot>),
|
||||||
WallpaperLoaded(u64, Result<widget::image::Handle, String>),
|
WallpaperLoaded(u64, Result<widget::image::Handle, String>),
|
||||||
Event(Box<Event>),
|
Event(Box<Event>),
|
||||||
Input(String),
|
Input(String),
|
||||||
@@ -193,7 +193,7 @@ impl cosmic::Application for Locker {
|
|||||||
if self.preview {
|
if self.preview {
|
||||||
Subscription::none()
|
Subscription::none()
|
||||||
} else {
|
} else {
|
||||||
ferese_theme::service::subscription().map(Message::ThemeChanged)
|
ferese_theme::service::subscription().map(|snapshot| Message::ThemeChanged(Box::new(snapshot)))
|
||||||
},
|
},
|
||||||
event::listen_with(|event, _, _| match event {
|
event::listen_with(|event, _, _| match event {
|
||||||
Event::PlatformSpecific(PlatformSpecific::Wayland(
|
Event::PlatformSpecific(PlatformSpecific::Wayland(
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ enum Message {
|
|||||||
RefreshDisplays,
|
RefreshDisplays,
|
||||||
DisplaysLoaded(Result<Vec<displays::Display>, String>),
|
DisplaysLoaded(Result<Vec<displays::Display>, String>),
|
||||||
RefreshRate(String, displays::Mode, bool),
|
RefreshRate(String, displays::Mode, bool),
|
||||||
ThemeChanged(ferese_config::theme::Snapshot),
|
ThemeChanged(Box<ferese_config::theme::Snapshot>),
|
||||||
DragWindow,
|
DragWindow,
|
||||||
Page(Page),
|
Page(Page),
|
||||||
Search(String),
|
Search(String),
|
||||||
@@ -207,7 +207,7 @@ impl cosmic::Application for App {
|
|||||||
return self.edit_many(displays::edits(&prefix, mode, automatic));
|
return self.edit_many(displays::edits(&prefix, mode, automatic));
|
||||||
}
|
}
|
||||||
Message::ThemeChanged(snapshot) => {
|
Message::ThemeChanged(snapshot) => {
|
||||||
self.resolved = snapshot;
|
self.resolved = *snapshot;
|
||||||
self.font = ferese_theme::font(Some(&self.resolved.presented.tokens.typography.font_family));
|
self.font = ferese_theme::font(Some(&self.resolved.presented.tokens.typography.font_family));
|
||||||
return self.update_theme();
|
return self.update_theme();
|
||||||
}
|
}
|
||||||
@@ -596,8 +596,8 @@ impl cosmic::Application for App {
|
|||||||
|
|
||||||
fn subscription(&self) -> cosmic::iced::Subscription<Message> {
|
fn subscription(&self) -> cosmic::iced::Subscription<Message> {
|
||||||
cosmic::iced::Subscription::batch([
|
cosmic::iced::Subscription::batch([
|
||||||
cosmic::iced::Subscription::run_with(self.path.clone(), watch::changes),
|
cosmic::iced::Subscription::run_with(self.path.clone(), |path| watch::changes(path)),
|
||||||
ferese_theme::service::subscription().map(Message::ThemeChanged),
|
ferese_theme::service::subscription().map(|snapshot| Message::ThemeChanged(Box::new(snapshot))),
|
||||||
if self.page == Page::Displays {
|
if self.page == Page::Displays {
|
||||||
cosmic::iced::time::every(std::time::Duration::from_secs(2)).map(|_| Message::RefreshDisplays)
|
cosmic::iced::time::every(std::time::Duration::from_secs(2)).map(|_| Message::RefreshDisplays)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -412,12 +412,14 @@ impl App {
|
|||||||
format!("{prefix}.scale"),
|
format!("{prefix}.scale"),
|
||||||
"Scale",
|
"Scale",
|
||||||
"Logical size of text and controls.",
|
"Logical size of text and controls.",
|
||||||
1.,
|
schema::RangeSpec {
|
||||||
0.75,
|
default: 1.,
|
||||||
3.,
|
min: 0.75,
|
||||||
0.25,
|
max: 3.,
|
||||||
"×",
|
step: 0.25,
|
||||||
false,
|
suffix: "×",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
)));
|
)));
|
||||||
body = body.push(container(group).padding(8).class(visuals::surface(palette.card, 14.)));
|
body = body.push(container(group).padding(8).class(visuals::surface(palette.card, 14.)));
|
||||||
}
|
}
|
||||||
|
|||||||
+347
-252
@@ -148,17 +148,30 @@ fn toggle(path: &str, label: &str, description: &str, default: bool) -> Field {
|
|||||||
Field::new(path, label, description, Kind::Toggle(default))
|
Field::new(path, label, description, Kind::Toggle(default))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct RangeSpec {
|
||||||
|
pub default: f64,
|
||||||
|
pub min: f64,
|
||||||
|
pub max: f64,
|
||||||
|
pub step: f64,
|
||||||
|
pub suffix: &'static str,
|
||||||
|
pub integer: bool,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn range(
|
pub fn range(
|
||||||
path: impl Into<String>,
|
path: impl Into<String>,
|
||||||
label: impl Into<String>,
|
label: impl Into<String>,
|
||||||
description: impl Into<String>,
|
description: impl Into<String>,
|
||||||
default: f64,
|
spec: RangeSpec,
|
||||||
min: f64,
|
|
||||||
max: f64,
|
|
||||||
step: f64,
|
|
||||||
suffix: &'static str,
|
|
||||||
integer: bool,
|
|
||||||
) -> Field {
|
) -> Field {
|
||||||
|
let RangeSpec {
|
||||||
|
default,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
step,
|
||||||
|
suffix,
|
||||||
|
integer,
|
||||||
|
} = spec;
|
||||||
Field::new(
|
Field::new(
|
||||||
path,
|
path,
|
||||||
label,
|
label,
|
||||||
@@ -278,100 +291,118 @@ fn clock_fields() -> Vec<Field> {
|
|||||||
"desktop_widgets.clock.margin_x",
|
"desktop_widgets.clock.margin_x",
|
||||||
"Horizontal margin",
|
"Horizontal margin",
|
||||||
"Distance from left/right edge; ignored when horizontally centered.",
|
"Distance from left/right edge; ignored when horizontally centered.",
|
||||||
64.0,
|
RangeSpec {
|
||||||
0.0,
|
default: 64.0,
|
||||||
1000.0,
|
min: 0.0,
|
||||||
1.0,
|
max: 1000.0,
|
||||||
" px",
|
step: 1.0,
|
||||||
true,
|
suffix: " px",
|
||||||
|
integer: true,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
"desktop_widgets.clock.margin_y",
|
"desktop_widgets.clock.margin_y",
|
||||||
"Vertical margin",
|
"Vertical margin",
|
||||||
"Distance from top/bottom edge; ignored when vertically centered.",
|
"Distance from top/bottom edge; ignored when vertically centered.",
|
||||||
80.0,
|
RangeSpec {
|
||||||
0.0,
|
default: 80.0,
|
||||||
1000.0,
|
min: 0.0,
|
||||||
1.0,
|
max: 1000.0,
|
||||||
" px",
|
step: 1.0,
|
||||||
true,
|
suffix: " px",
|
||||||
|
integer: true,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
"desktop_widgets.clock.width",
|
"desktop_widgets.clock.width",
|
||||||
"Widget width",
|
"Widget width",
|
||||||
"Logical pixels; increase for longer formats.",
|
"Logical pixels; increase for longer formats.",
|
||||||
440.0,
|
RangeSpec {
|
||||||
64.0,
|
default: 440.0,
|
||||||
1600.0,
|
min: 64.0,
|
||||||
1.0,
|
max: 1600.0,
|
||||||
" px",
|
step: 1.0,
|
||||||
true,
|
suffix: " px",
|
||||||
|
integer: true,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
"desktop_widgets.clock.height",
|
"desktop_widgets.clock.height",
|
||||||
"Widget height",
|
"Widget height",
|
||||||
"Leave room for both text lines.",
|
"Leave room for both text lines.",
|
||||||
320.0,
|
RangeSpec {
|
||||||
32.0,
|
default: 320.0,
|
||||||
800.0,
|
min: 32.0,
|
||||||
1.0,
|
max: 800.0,
|
||||||
" px",
|
step: 1.0,
|
||||||
true,
|
suffix: " px",
|
||||||
|
integer: true,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
"desktop_widgets.clock.time_size",
|
"desktop_widgets.clock.time_size",
|
||||||
"Time size",
|
"Time size",
|
||||||
"Logical pixels; scales with your output.",
|
"Logical pixels; scales with your output.",
|
||||||
128.0,
|
RangeSpec {
|
||||||
8.0,
|
default: 128.0,
|
||||||
240.0,
|
min: 8.0,
|
||||||
1.0,
|
max: 240.0,
|
||||||
" px",
|
step: 1.0,
|
||||||
false,
|
suffix: " px",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
"desktop_widgets.clock.date_size",
|
"desktop_widgets.clock.date_size",
|
||||||
"Date size",
|
"Date size",
|
||||||
"Separate size for the date line.",
|
"Separate size for the date line.",
|
||||||
18.0,
|
RangeSpec {
|
||||||
8.0,
|
default: 18.0,
|
||||||
96.0,
|
min: 8.0,
|
||||||
1.0,
|
max: 96.0,
|
||||||
" px",
|
step: 1.0,
|
||||||
false,
|
suffix: " px",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
"desktop_widgets.clock.opacity",
|
"desktop_widgets.clock.opacity",
|
||||||
"Opacity",
|
"Opacity",
|
||||||
"Applies to text and the optional background.",
|
"Applies to text and the optional background.",
|
||||||
0.9,
|
RangeSpec {
|
||||||
0.0,
|
default: 0.9,
|
||||||
1.0,
|
min: 0.0,
|
||||||
0.05,
|
max: 1.0,
|
||||||
"",
|
step: 0.05,
|
||||||
false,
|
suffix: "",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
"desktop_widgets.clock.gap",
|
"desktop_widgets.clock.gap",
|
||||||
"Line spacing",
|
"Line spacing",
|
||||||
"Space between time and date.",
|
"Space between time and date.",
|
||||||
4.0,
|
RangeSpec {
|
||||||
0.0,
|
default: 4.0,
|
||||||
64.0,
|
min: 0.0,
|
||||||
1.0,
|
max: 64.0,
|
||||||
" px",
|
step: 1.0,
|
||||||
false,
|
suffix: " px",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
"desktop_widgets.clock.padding",
|
"desktop_widgets.clock.padding",
|
||||||
"Inner padding",
|
"Inner padding",
|
||||||
"Space around the labels.",
|
"Space around the labels.",
|
||||||
12.0,
|
RangeSpec {
|
||||||
0.0,
|
default: 12.0,
|
||||||
64.0,
|
min: 0.0,
|
||||||
1.0,
|
max: 64.0,
|
||||||
" px",
|
step: 1.0,
|
||||||
false,
|
suffix: " px",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
choice(
|
choice(
|
||||||
"desktop_widgets.clock.alignment",
|
"desktop_widgets.clock.alignment",
|
||||||
@@ -422,100 +453,118 @@ pub fn note_fields(index: usize) -> Vec<Field> {
|
|||||||
format!("{prefix}.width"),
|
format!("{prefix}.width"),
|
||||||
"Width",
|
"Width",
|
||||||
"Logical pixels; centered axes ignore margins.",
|
"Logical pixels; centered axes ignore margins.",
|
||||||
320.0,
|
RangeSpec {
|
||||||
120.0,
|
default: 320.0,
|
||||||
1200.0,
|
min: 120.0,
|
||||||
1.0,
|
max: 1200.0,
|
||||||
" px",
|
step: 1.0,
|
||||||
true,
|
suffix: " px",
|
||||||
|
integer: true,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
format!("{prefix}.height"),
|
format!("{prefix}.height"),
|
||||||
"Height",
|
"Height",
|
||||||
"Logical pixels; centered axes ignore margins.",
|
"Logical pixels; centered axes ignore margins.",
|
||||||
240.0,
|
RangeSpec {
|
||||||
80.0,
|
default: 240.0,
|
||||||
1200.0,
|
min: 80.0,
|
||||||
1.0,
|
max: 1200.0,
|
||||||
" px",
|
step: 1.0,
|
||||||
true,
|
suffix: " px",
|
||||||
|
integer: true,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
format!("{prefix}.margin_x"),
|
format!("{prefix}.margin_x"),
|
||||||
"Horizontal margin",
|
"Horizontal margin",
|
||||||
"Logical pixels; centered axes ignore margins.",
|
"Logical pixels; centered axes ignore margins.",
|
||||||
48.0,
|
RangeSpec {
|
||||||
0.0,
|
default: 48.0,
|
||||||
1000.0,
|
min: 0.0,
|
||||||
1.0,
|
max: 1000.0,
|
||||||
" px",
|
step: 1.0,
|
||||||
true,
|
suffix: " px",
|
||||||
|
integer: true,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
format!("{prefix}.margin_y"),
|
format!("{prefix}.margin_y"),
|
||||||
"Vertical margin",
|
"Vertical margin",
|
||||||
"Logical pixels; centered axes ignore margins.",
|
"Logical pixels; centered axes ignore margins.",
|
||||||
80.0,
|
RangeSpec {
|
||||||
0.0,
|
default: 80.0,
|
||||||
1000.0,
|
min: 0.0,
|
||||||
1.0,
|
max: 1000.0,
|
||||||
" px",
|
step: 1.0,
|
||||||
true,
|
suffix: " px",
|
||||||
|
integer: true,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
format!("{prefix}.text_size"),
|
format!("{prefix}.text_size"),
|
||||||
"Text size",
|
"Text size",
|
||||||
"Logical pixels; centered axes ignore margins.",
|
"Logical pixels; centered axes ignore margins.",
|
||||||
16.0,
|
RangeSpec {
|
||||||
8.0,
|
default: 16.0,
|
||||||
96.0,
|
min: 8.0,
|
||||||
1.0,
|
max: 96.0,
|
||||||
" px",
|
step: 1.0,
|
||||||
false,
|
suffix: " px",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
format!("{prefix}.title_size"),
|
format!("{prefix}.title_size"),
|
||||||
"Title size",
|
"Title size",
|
||||||
"Logical pixels; centered axes ignore margins.",
|
"Logical pixels; centered axes ignore margins.",
|
||||||
18.0,
|
RangeSpec {
|
||||||
8.0,
|
default: 18.0,
|
||||||
96.0,
|
min: 8.0,
|
||||||
1.0,
|
max: 96.0,
|
||||||
" px",
|
step: 1.0,
|
||||||
false,
|
suffix: " px",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
format!("{prefix}.opacity"),
|
format!("{prefix}.opacity"),
|
||||||
"Opacity",
|
"Opacity",
|
||||||
"Logical pixels; centered axes ignore margins.",
|
"Logical pixels; centered axes ignore margins.",
|
||||||
0.9,
|
RangeSpec {
|
||||||
0.0,
|
default: 0.9,
|
||||||
1.0,
|
min: 0.0,
|
||||||
0.05,
|
max: 1.0,
|
||||||
"",
|
step: 0.05,
|
||||||
false,
|
suffix: "",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
format!("{prefix}.padding"),
|
format!("{prefix}.padding"),
|
||||||
"Padding",
|
"Padding",
|
||||||
"Logical pixels; centered axes ignore margins.",
|
"Logical pixels; centered axes ignore margins.",
|
||||||
20.0,
|
RangeSpec {
|
||||||
0.0,
|
default: 20.0,
|
||||||
64.0,
|
min: 0.0,
|
||||||
1.0,
|
max: 64.0,
|
||||||
" px",
|
step: 1.0,
|
||||||
false,
|
suffix: " px",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
format!("{prefix}.gap"),
|
format!("{prefix}.gap"),
|
||||||
"Title spacing",
|
"Title spacing",
|
||||||
"Logical pixels; centered axes ignore margins.",
|
"Logical pixels; centered axes ignore margins.",
|
||||||
8.0,
|
RangeSpec {
|
||||||
0.0,
|
default: 8.0,
|
||||||
64.0,
|
min: 0.0,
|
||||||
1.0,
|
max: 64.0,
|
||||||
" px",
|
step: 1.0,
|
||||||
false,
|
suffix: " px",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
Field::new(
|
Field::new(
|
||||||
format!("{prefix}.alignment"),
|
format!("{prefix}.alignment"),
|
||||||
@@ -536,23 +585,27 @@ pub fn fields(page: Page) -> Vec<Field> {
|
|||||||
"lock_screen.dim_after_seconds",
|
"lock_screen.dim_after_seconds",
|
||||||
"Dim after",
|
"Dim after",
|
||||||
"Dim the locked display after inactivity. Zero disables dimming.",
|
"Dim the locked display after inactivity. Zero disables dimming.",
|
||||||
30.0,
|
RangeSpec {
|
||||||
0.0,
|
default: 30.0,
|
||||||
600.0,
|
min: 0.0,
|
||||||
5.0,
|
max: 600.0,
|
||||||
"s",
|
step: 5.0,
|
||||||
true,
|
suffix: "s",
|
||||||
|
integer: true,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
"lock_screen.sleep_after_seconds",
|
"lock_screen.sleep_after_seconds",
|
||||||
"Display sleep after",
|
"Display sleep after",
|
||||||
"Turn off locked displays after inactivity. Input wakes them; zero disables display sleep.",
|
"Turn off locked displays after inactivity. Input wakes them; zero disables display sleep.",
|
||||||
120.0,
|
RangeSpec {
|
||||||
0.0,
|
default: 120.0,
|
||||||
1800.0,
|
min: 0.0,
|
||||||
15.0,
|
max: 1800.0,
|
||||||
"s",
|
step: 15.0,
|
||||||
true,
|
suffix: "s",
|
||||||
|
integer: true,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
toggle(
|
toggle(
|
||||||
"lock_screen.show_clock",
|
"lock_screen.show_clock",
|
||||||
@@ -577,23 +630,27 @@ pub fn fields(page: Page) -> Vec<Field> {
|
|||||||
"lock_screen.background_blur",
|
"lock_screen.background_blur",
|
||||||
"Wallpaper softness",
|
"Wallpaper softness",
|
||||||
"Blur the lock screen wallpaper. Zero keeps the original image sharp.",
|
"Blur the lock screen wallpaper. Zero keeps the original image sharp.",
|
||||||
18.0,
|
RangeSpec {
|
||||||
0.0,
|
default: 18.0,
|
||||||
40.0,
|
min: 0.0,
|
||||||
1.0,
|
max: 40.0,
|
||||||
"",
|
step: 1.0,
|
||||||
false,
|
suffix: "",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
"lock_screen.background_dim",
|
"lock_screen.background_dim",
|
||||||
"Wallpaper dimming",
|
"Wallpaper dimming",
|
||||||
"Darken the wallpaper behind the clock and sign-in card.",
|
"Darken the wallpaper behind the clock and sign-in card.",
|
||||||
0.48,
|
RangeSpec {
|
||||||
0.0,
|
default: 0.48,
|
||||||
0.9,
|
min: 0.0,
|
||||||
0.02,
|
max: 0.9,
|
||||||
"",
|
step: 0.02,
|
||||||
false,
|
suffix: "",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
Page::Accessibility => vec![
|
Page::Accessibility => vec![
|
||||||
@@ -664,12 +721,14 @@ pub fn fields(page: Page) -> Vec<Field> {
|
|||||||
"theme.geometry.shell_radius",
|
"theme.geometry.shell_radius",
|
||||||
"Shell corner radius",
|
"Shell corner radius",
|
||||||
"Rounds the bar, menus, notifications, desktop widgets and hover backgrounds. Window corners are separate.",
|
"Rounds the bar, menus, notifications, desktop widgets and hover backgrounds. Window corners are separate.",
|
||||||
14.0,
|
RangeSpec {
|
||||||
0.0,
|
default: 14.0,
|
||||||
32.0,
|
min: 0.0,
|
||||||
1.0,
|
max: 32.0,
|
||||||
" px",
|
step: 1.0,
|
||||||
false,
|
suffix: " px",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
text(
|
text(
|
||||||
"theme.accent",
|
"theme.accent",
|
||||||
@@ -688,23 +747,27 @@ pub fn fields(page: Page) -> Vec<Field> {
|
|||||||
"theme.material.opacity",
|
"theme.material.opacity",
|
||||||
"Shell opacity",
|
"Shell opacity",
|
||||||
"Background opacity for the bar, popovers, menus, notifications and sharing dialog. Text and icons stay opaque.",
|
"Background opacity for the bar, popovers, menus, notifications and sharing dialog. Text and icons stay opaque.",
|
||||||
ferese_config::DEFAULT_MATERIAL_OPACITY,
|
RangeSpec {
|
||||||
0.0,
|
default: ferese_config::DEFAULT_MATERIAL_OPACITY,
|
||||||
1.0,
|
min: 0.0,
|
||||||
0.01,
|
max: 1.0,
|
||||||
"",
|
step: 0.01,
|
||||||
false,
|
suffix: "",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
"theme.material.blur_radius",
|
"theme.material.blur_radius",
|
||||||
"Background blur",
|
"Background blur",
|
||||||
"Softness behind translucent surfaces.",
|
"Softness behind translucent surfaces.",
|
||||||
12.0,
|
RangeSpec {
|
||||||
0.0,
|
default: 12.0,
|
||||||
32.0,
|
min: 0.0,
|
||||||
1.0,
|
max: 32.0,
|
||||||
" px",
|
step: 1.0,
|
||||||
false,
|
suffix: " px",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
text(
|
text(
|
||||||
"theme.typography.font_family",
|
"theme.typography.font_family",
|
||||||
@@ -716,12 +779,14 @@ pub fn fields(page: Page) -> Vec<Field> {
|
|||||||
"theme.geometry.focus_ring_width",
|
"theme.geometry.focus_ring_width",
|
||||||
"Focus border",
|
"Focus border",
|
||||||
"A subtle outline around the active window.",
|
"A subtle outline around the active window.",
|
||||||
2.0,
|
RangeSpec {
|
||||||
0.0,
|
default: 2.0,
|
||||||
4.0,
|
min: 0.0,
|
||||||
0.5,
|
max: 4.0,
|
||||||
" px",
|
step: 0.5,
|
||||||
false,
|
suffix: " px",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
Page::Wallpaper => vec![
|
Page::Wallpaper => vec![
|
||||||
@@ -757,12 +822,14 @@ pub fn fields(page: Page) -> Vec<Field> {
|
|||||||
"notifications.timeout_ms",
|
"notifications.timeout_ms",
|
||||||
"Popup timeout",
|
"Popup timeout",
|
||||||
"Default duration; apps can request another timeout. Pauses while hovered.",
|
"Default duration; apps can request another timeout. Pauses while hovered.",
|
||||||
6000.0,
|
RangeSpec {
|
||||||
1000.0,
|
default: 6000.0,
|
||||||
30000.0,
|
min: 1000.0,
|
||||||
1000.0,
|
max: 30000.0,
|
||||||
" ms",
|
step: 1000.0,
|
||||||
true,
|
suffix: " ms",
|
||||||
|
integer: true,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
Page::Bar => vec![
|
Page::Bar => vec![
|
||||||
@@ -776,45 +843,53 @@ pub fn fields(page: Page) -> Vec<Field> {
|
|||||||
"theme.geometry.top_bar_height",
|
"theme.geometry.top_bar_height",
|
||||||
"Height",
|
"Height",
|
||||||
"Keeps icon and text sizes unchanged.",
|
"Keeps icon and text sizes unchanged.",
|
||||||
28.0,
|
RangeSpec {
|
||||||
24.0,
|
default: 28.0,
|
||||||
48.0,
|
min: 24.0,
|
||||||
1.0,
|
max: 48.0,
|
||||||
" px",
|
step: 1.0,
|
||||||
false,
|
suffix: " px",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
"theme.geometry.top_bar_margin_top",
|
"theme.geometry.top_bar_margin_top",
|
||||||
"Top margin",
|
"Top margin",
|
||||||
"Set to zero for an edge-to-edge bar.",
|
"Set to zero for an edge-to-edge bar.",
|
||||||
0.0,
|
RangeSpec {
|
||||||
0.0,
|
default: 0.0,
|
||||||
24.0,
|
min: 0.0,
|
||||||
1.0,
|
max: 24.0,
|
||||||
" px",
|
step: 1.0,
|
||||||
true,
|
suffix: " px",
|
||||||
|
integer: true,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
"theme.geometry.top_bar_margin_horizontal",
|
"theme.geometry.top_bar_margin_horizontal",
|
||||||
"Side margins",
|
"Side margins",
|
||||||
"Space between the bar and the display edges.",
|
"Space between the bar and the display edges.",
|
||||||
0.0,
|
RangeSpec {
|
||||||
0.0,
|
default: 0.0,
|
||||||
32.0,
|
min: 0.0,
|
||||||
1.0,
|
max: 32.0,
|
||||||
" px",
|
step: 1.0,
|
||||||
true,
|
suffix: " px",
|
||||||
|
integer: true,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
"theme.geometry.top_bar_window_gap",
|
"theme.geometry.top_bar_window_gap",
|
||||||
"Window clearance",
|
"Window clearance",
|
||||||
"Space below the menu bar.",
|
"Space below the menu bar.",
|
||||||
0.0,
|
RangeSpec {
|
||||||
0.0,
|
default: 0.0,
|
||||||
24.0,
|
min: 0.0,
|
||||||
1.0,
|
max: 24.0,
|
||||||
" px",
|
step: 1.0,
|
||||||
true,
|
suffix: " px",
|
||||||
|
integer: true,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
toggle(
|
toggle(
|
||||||
"status.battery_percentage",
|
"status.battery_percentage",
|
||||||
@@ -852,45 +927,53 @@ pub fn fields(page: Page) -> Vec<Field> {
|
|||||||
"scrolling.default_column_width",
|
"scrolling.default_column_width",
|
||||||
"New window width",
|
"New window width",
|
||||||
"Fraction of the available workspace.",
|
"Fraction of the available workspace.",
|
||||||
0.5,
|
RangeSpec {
|
||||||
0.25,
|
default: 0.5,
|
||||||
1.0,
|
min: 0.25,
|
||||||
0.05,
|
max: 1.0,
|
||||||
"",
|
step: 0.05,
|
||||||
false,
|
suffix: "",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
"layout.inner_gap",
|
"layout.inner_gap",
|
||||||
"Between windows",
|
"Between windows",
|
||||||
"Space between neighboring windows.",
|
"Space between neighboring windows.",
|
||||||
10.0,
|
RangeSpec {
|
||||||
0.0,
|
default: 10.0,
|
||||||
32.0,
|
min: 0.0,
|
||||||
1.0,
|
max: 32.0,
|
||||||
" px",
|
step: 1.0,
|
||||||
false,
|
suffix: " px",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
"layout.outer_gap",
|
"layout.outer_gap",
|
||||||
"Workspace edges",
|
"Workspace edges",
|
||||||
"Space around the outside of the layout.",
|
"Space around the outside of the layout.",
|
||||||
4.0,
|
RangeSpec {
|
||||||
0.0,
|
default: 4.0,
|
||||||
32.0,
|
min: 0.0,
|
||||||
1.0,
|
max: 32.0,
|
||||||
" px",
|
step: 1.0,
|
||||||
false,
|
suffix: " px",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
"theme.geometry.window_radius",
|
"theme.geometry.window_radius",
|
||||||
"Corner radius",
|
"Corner radius",
|
||||||
"Rounded corners for application windows.",
|
"Rounded corners for application windows.",
|
||||||
14.0,
|
RangeSpec {
|
||||||
0.0,
|
default: 14.0,
|
||||||
28.0,
|
min: 0.0,
|
||||||
1.0,
|
max: 28.0,
|
||||||
" px",
|
step: 1.0,
|
||||||
false,
|
suffix: " px",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
toggle(
|
toggle(
|
||||||
"appearance.inactive_dim.enabled",
|
"appearance.inactive_dim.enabled",
|
||||||
@@ -902,12 +985,14 @@ pub fn fields(page: Page) -> Vec<Field> {
|
|||||||
"appearance.inactive_dim.amount",
|
"appearance.inactive_dim.amount",
|
||||||
"Dimming amount",
|
"Dimming amount",
|
||||||
"How much inactive windows recede.",
|
"How much inactive windows recede.",
|
||||||
0.15,
|
RangeSpec {
|
||||||
0.0,
|
default: 0.15,
|
||||||
0.5,
|
min: 0.0,
|
||||||
0.01,
|
max: 0.5,
|
||||||
"",
|
step: 0.01,
|
||||||
false,
|
suffix: "",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
Page::Motion => vec![
|
Page::Motion => vec![
|
||||||
@@ -921,23 +1006,27 @@ pub fn fields(page: Page) -> Vec<Field> {
|
|||||||
"animations.speed",
|
"animations.speed",
|
||||||
"Animation speed",
|
"Animation speed",
|
||||||
"Lower values give transitions more time.",
|
"Lower values give transitions more time.",
|
||||||
1.0,
|
RangeSpec {
|
||||||
0.25,
|
default: 1.0,
|
||||||
2.0,
|
min: 0.25,
|
||||||
0.05,
|
max: 2.0,
|
||||||
"×",
|
step: 0.05,
|
||||||
false,
|
suffix: "×",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
"appearance.inactive_dim.duration_ms",
|
"appearance.inactive_dim.duration_ms",
|
||||||
"Focus transition",
|
"Focus transition",
|
||||||
"Time taken to dim an inactive window.",
|
"Time taken to dim an inactive window.",
|
||||||
150.0,
|
RangeSpec {
|
||||||
0.0,
|
default: 150.0,
|
||||||
600.0,
|
min: 0.0,
|
||||||
10.0,
|
max: 600.0,
|
||||||
" ms",
|
step: 10.0,
|
||||||
false,
|
suffix: " ms",
|
||||||
|
integer: false,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
Page::Keyboard => vec![
|
Page::Keyboard => vec![
|
||||||
@@ -957,23 +1046,27 @@ pub fn fields(page: Page) -> Vec<Field> {
|
|||||||
"input.repeat_rate",
|
"input.repeat_rate",
|
||||||
"Key repeat",
|
"Key repeat",
|
||||||
"Characters repeated each second while a key is held.",
|
"Characters repeated each second while a key is held.",
|
||||||
25.0,
|
RangeSpec {
|
||||||
1.0,
|
default: 25.0,
|
||||||
70.0,
|
min: 1.0,
|
||||||
1.0,
|
max: 70.0,
|
||||||
" /s",
|
step: 1.0,
|
||||||
true,
|
suffix: " /s",
|
||||||
|
integer: true,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
range(
|
range(
|
||||||
"input.repeat_delay_ms",
|
"input.repeat_delay_ms",
|
||||||
"Delay before repeat",
|
"Delay before repeat",
|
||||||
"How long to hold a key before it starts repeating.",
|
"How long to hold a key before it starts repeating.",
|
||||||
600.0,
|
RangeSpec {
|
||||||
150.0,
|
default: 600.0,
|
||||||
1000.0,
|
min: 150.0,
|
||||||
25.0,
|
max: 1000.0,
|
||||||
" ms",
|
step: 25.0,
|
||||||
true,
|
suffix: " ms",
|
||||||
|
integer: true,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
toggle(
|
toggle(
|
||||||
"input.focus_follows_mouse",
|
"input.focus_follows_mouse",
|
||||||
@@ -1003,12 +1096,14 @@ pub fn fields(page: Page) -> Vec<Field> {
|
|||||||
"input.touchpad.swipe_threshold",
|
"input.touchpad.swipe_threshold",
|
||||||
"Swipe distance",
|
"Swipe distance",
|
||||||
"Distance needed to navigate. Larger values reduce accidental swipes.",
|
"Distance needed to navigate. Larger values reduce accidental swipes.",
|
||||||
80.0,
|
RangeSpec {
|
||||||
16.0,
|
default: 80.0,
|
||||||
1000.0,
|
min: 16.0,
|
||||||
8.0,
|
max: 1000.0,
|
||||||
" px",
|
step: 8.0,
|
||||||
true,
|
suffix: " px",
|
||||||
|
integer: true,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
Page::Shortcuts => vec![toggle(
|
Page::Shortcuts => vec![toggle(
|
||||||
|
|||||||
@@ -199,12 +199,14 @@ impl App {
|
|||||||
let tile_id = gallery_id(&id, appearance);
|
let tile_id = gallery_id(&id, appearance);
|
||||||
tiles = tiles.push(gallery::tile(
|
tiles = tiles.push(gallery::tile(
|
||||||
family,
|
family,
|
||||||
appearance,
|
gallery::TileOptions {
|
||||||
|
variant: appearance,
|
||||||
active,
|
active,
|
||||||
selected,
|
selected,
|
||||||
palette,
|
palette,
|
||||||
self.font,
|
font: self.font,
|
||||||
tile_id,
|
id: tile_id,
|
||||||
|
},
|
||||||
Message::Family(id, appearance),
|
Message::Family(id, appearance),
|
||||||
move |key| {
|
move |key| {
|
||||||
gallery::neighbor(index, ids.len(), key)
|
gallery::neighbor(index, ids.len(), key)
|
||||||
|
|||||||
@@ -294,9 +294,11 @@ mod tests {
|
|||||||
fn presets_are_distinct_and_readable_on_settings_surfaces() {
|
fn presets_are_distinct_and_readable_on_settings_surfaces() {
|
||||||
for (index, preset) in PRESETS.iter().enumerate() {
|
for (index, preset) in PRESETS.iter().enumerate() {
|
||||||
let tokens = ferese_config::theme::preset(preset.id, preset.appearance).unwrap();
|
let tokens = ferese_config::theme::preset(preset.id, preset.appearance).unwrap();
|
||||||
let mut theme = ferese_config::theme::ResolvedTheme::default();
|
let theme = ferese_config::theme::ResolvedTheme {
|
||||||
theme.appearance = preset.appearance;
|
appearance: preset.appearance,
|
||||||
theme.tokens = tokens;
|
tokens,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
let palette = Palette::from_resolved(&theme);
|
let palette = Palette::from_resolved(&theme);
|
||||||
for background in [palette.background, palette.sidebar, palette.card] {
|
for background in [palette.background, palette.sidebar, palette.card] {
|
||||||
assert!(contrast(palette.text, background) >= 4.5, "{} text", preset.name);
|
assert!(contrast(palette.text, background) >= 4.5, "{} text", preset.name);
|
||||||
@@ -313,10 +315,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn selection_tracks_all_preset_colors_and_gradient_changes() {
|
fn selection_tracks_all_preset_colors_and_gradient_changes() {
|
||||||
for index in 0..PRESETS.len() {
|
for (index, preset) in PRESETS.iter().enumerate() {
|
||||||
let mut snapshot = configured_preset(index);
|
let mut snapshot = configured_preset(index);
|
||||||
assert!(preset_selected(&snapshot, index));
|
assert!(preset_selected(&snapshot, index));
|
||||||
let kind = if PRESETS[index].appearance == ferese_config::theme::Appearance::Light {
|
let kind = if preset.appearance == ferese_config::theme::Appearance::Light {
|
||||||
"light"
|
"light"
|
||||||
} else {
|
} else {
|
||||||
"dark"
|
"dark"
|
||||||
@@ -343,9 +345,11 @@ mod tests {
|
|||||||
|
|
||||||
for (index, item) in PRESETS.iter().enumerate() {
|
for (index, item) in PRESETS.iter().enumerate() {
|
||||||
let _ = index;
|
let _ = index;
|
||||||
let mut resolved = ferese_config::theme::ResolvedTheme::default();
|
let resolved = ferese_config::theme::ResolvedTheme {
|
||||||
resolved.appearance = item.appearance;
|
appearance: item.appearance,
|
||||||
resolved.tokens = ferese_config::theme::preset(item.id, item.appearance).unwrap();
|
tokens: ferese_config::theme::preset(item.id, item.appearance).unwrap(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Palette::from_resolved(&resolved).native_theme().cosmic().is_dark,
|
Palette::from_resolved(&resolved).native_theme().cosmic().is_dark,
|
||||||
item.appearance == ferese_config::theme::Appearance::Dark
|
item.appearance == ferese_config::theme::Appearance::Dark
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
//! Watch the parent directory so atomic editor saves keep working.
|
//! Watch the parent directory so atomic editor saves keep working.
|
||||||
use std::path::PathBuf;
|
use std::path::Path;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use cosmic::iced::futures::{SinkExt, Stream};
|
use cosmic::iced::futures::{SinkExt, Stream};
|
||||||
@@ -8,8 +8,8 @@ use notify::{RecursiveMode, Watcher};
|
|||||||
use crate::Message;
|
use crate::Message;
|
||||||
use crate::store::Snapshot;
|
use crate::store::Snapshot;
|
||||||
|
|
||||||
pub fn changes(path: &PathBuf) -> impl Stream<Item = Message> + use<> {
|
pub fn changes(path: &Path) -> impl Stream<Item = Message> + use<> {
|
||||||
let path = path.clone();
|
let path = path.to_path_buf();
|
||||||
|
|
||||||
cosmic::iced::stream::channel(1, async move |mut output| {
|
cosmic::iced::stream::channel(1, async move |mut output| {
|
||||||
let (send, mut events) = tokio::sync::mpsc::channel(1);
|
let (send, mut events) = tokio::sync::mpsc::channel(1);
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ pub(crate) struct WorkspaceSnapshot {
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub(crate) struct WindowSnapshot {
|
pub(crate) struct WindowSnapshot {
|
||||||
pub(crate) id: u64,
|
|
||||||
pub(crate) workspace: u64,
|
pub(crate) workspace: u64,
|
||||||
pub(crate) app_id: String,
|
pub(crate) app_id: String,
|
||||||
pub(crate) title: String,
|
pub(crate) title: String,
|
||||||
@@ -138,12 +137,6 @@ impl ShellControl {
|
|||||||
let _ = self.connection.flush();
|
let _ = self.connection.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn activate_window(&self, id: u64) {
|
|
||||||
let (hi, lo) = split_id(id);
|
|
||||||
self.shell.activate_window(hi, lo);
|
|
||||||
let _ = self.connection.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn confirm_logout(&self, serial: u32, revision: u32, token: u32, force: bool) {
|
pub(crate) fn confirm_logout(&self, serial: u32, revision: u32, token: u32, force: bool) {
|
||||||
self.shell
|
self.shell
|
||||||
.confirm_logout_with_inhibitors(serial, revision, token, u32::from(force));
|
.confirm_logout_with_inhibitors(serial, revision, token, u32::from(force));
|
||||||
@@ -268,8 +261,8 @@ impl Dispatch<FereseShellV1, ()> for ControlState {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
ferese_shell_v1::Event::Window {
|
ferese_shell_v1::Event::Window {
|
||||||
window_hi,
|
window_hi: _,
|
||||||
window_lo,
|
window_lo: _,
|
||||||
workspace_hi,
|
workspace_hi,
|
||||||
workspace_lo,
|
workspace_lo,
|
||||||
app_id,
|
app_id,
|
||||||
@@ -285,7 +278,6 @@ impl Dispatch<FereseShellV1, ()> for ControlState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
state.pending.windows.push(WindowSnapshot {
|
state.pending.windows.push(WindowSnapshot {
|
||||||
id: join_id(window_hi, window_lo),
|
|
||||||
workspace: join_id(workspace_hi, workspace_lo),
|
workspace: join_id(workspace_hi, workspace_lo),
|
||||||
app_id,
|
app_id,
|
||||||
title,
|
title,
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ struct OutputSurfaces {
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
enum Message {
|
enum Message {
|
||||||
ThemeChanged(ferese_config::theme::Snapshot),
|
ThemeChanged(Box<ferese_config::theme::Snapshot>),
|
||||||
ThemeMode(ferese_config::theme::Mode),
|
ThemeMode(ferese_config::theme::Mode),
|
||||||
ThemeModeSet(Result<(), String>),
|
ThemeModeSet(Result<(), String>),
|
||||||
BeginNoteEdit(String),
|
BeginNoteEdit(String),
|
||||||
@@ -340,7 +340,7 @@ impl cosmic::Application for FereseShell {
|
|||||||
|
|
||||||
fn subscription(&self) -> Subscription<Self::Message> {
|
fn subscription(&self) -> Subscription<Self::Message> {
|
||||||
Subscription::batch([
|
Subscription::batch([
|
||||||
ferese_theme::service::subscription().map(Message::ThemeChanged),
|
ferese_theme::service::subscription().map(|snapshot| Message::ThemeChanged(Box::new(snapshot))),
|
||||||
if self
|
if self
|
||||||
.system_modal
|
.system_modal
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -1208,7 +1208,6 @@ mod tests {
|
|||||||
}],
|
}],
|
||||||
workspaces: Vec::new(),
|
workspaces: Vec::new(),
|
||||||
windows: vec![WindowSnapshot {
|
windows: vec![WindowSnapshot {
|
||||||
id: 1,
|
|
||||||
workspace,
|
workspace,
|
||||||
app_id: "dev.ferese.Test".to_owned(),
|
app_id: "dev.ferese.Test".to_owned(),
|
||||||
title: "Test".to_owned(),
|
title: "Test".to_owned(),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use super::bar::status_icon;
|
|||||||
use super::controls::status_summary;
|
use super::controls::status_summary;
|
||||||
use super::{Menu, MenuRows, MenuStyle};
|
use super::{Menu, MenuRows, MenuStyle};
|
||||||
use crate::status::Action;
|
use crate::status::Action;
|
||||||
use crate::{FereseShell, Message, color_with_opacity, text};
|
use crate::{FereseShell, Message, text};
|
||||||
|
|
||||||
const DEVICE_LIST_HEIGHT: f32 = 180.0;
|
const DEVICE_LIST_HEIGHT: f32 = 180.0;
|
||||||
|
|
||||||
@@ -15,12 +15,7 @@ fn device_list_height(count: usize) -> Option<f32> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: MenuStyle) -> MenuRows<'a> {
|
pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: MenuStyle) -> MenuRows<'a> {
|
||||||
let MenuStyle {
|
let MenuStyle { muted, .. } = style;
|
||||||
theme,
|
|
||||||
primary,
|
|
||||||
muted,
|
|
||||||
opacity: p,
|
|
||||||
} = style;
|
|
||||||
if let Some(b) = &shell.status.bluetooth {
|
if let Some(b) = &shell.status.bluetooth {
|
||||||
rows = rows.push(status_summary(
|
rows = rows.push(status_summary(
|
||||||
status_icon(Menu::Bluetooth, &shell.status).0,
|
status_icon(Menu::Bluetooth, &shell.status).0,
|
||||||
@@ -36,10 +31,7 @@ pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: Me
|
|||||||
} else {
|
} else {
|
||||||
"Your connected devices"
|
"Your connected devices"
|
||||||
},
|
},
|
||||||
primary,
|
style,
|
||||||
muted,
|
|
||||||
color_with_opacity(theme.accent, p),
|
|
||||||
p,
|
|
||||||
b.enabled,
|
b.enabled,
|
||||||
Some((b.enabled, Action::Bluetooth(!b.enabled))),
|
Some((b.enabled, Action::Bluetooth(!b.enabled))),
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -3,19 +3,23 @@ use cosmic::widget::{button, column, container, row, slider};
|
|||||||
use cosmic::{Element, theme};
|
use cosmic::{Element, theme};
|
||||||
|
|
||||||
use crate::status::Action;
|
use crate::status::Action;
|
||||||
use crate::{Message, ShellTheme, accented_icon, color, motion, shell_font, text};
|
use crate::{Message, ShellTheme, accented_icon, color, color_with_opacity, motion, shell_font, text};
|
||||||
|
|
||||||
pub(super) fn status_summary<'a>(
|
pub(super) fn status_summary<'a>(
|
||||||
source: &'static [u8],
|
source: &'static [u8],
|
||||||
title: &'a str,
|
title: &'a str,
|
||||||
subtitle: &'a str,
|
subtitle: &'a str,
|
||||||
primary: Color,
|
style: super::MenuStyle,
|
||||||
muted: Color,
|
|
||||||
accent: Color,
|
|
||||||
opacity: f32,
|
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
toggle: Option<(bool, Action)>,
|
toggle: Option<(bool, Action)>,
|
||||||
) -> Element<'a, cosmic::Action<Message>> {
|
) -> Element<'a, cosmic::Action<Message>> {
|
||||||
|
let super::MenuStyle {
|
||||||
|
theme,
|
||||||
|
primary,
|
||||||
|
muted,
|
||||||
|
opacity,
|
||||||
|
} = style;
|
||||||
|
let accent = color_with_opacity(theme.accent, opacity);
|
||||||
let badge_color = if enabled { accent } else { primary };
|
let badge_color = if enabled { accent } else { primary };
|
||||||
let badge = container(accented_icon(source, 22, primary, accent))
|
let badge = container(accented_icon(source, 22, primary, accent))
|
||||||
.width(32)
|
.width(32)
|
||||||
|
|||||||
@@ -5,14 +5,11 @@ use super::bar::status_icon;
|
|||||||
use super::controls::{level_meter, shell_switch, status_summary};
|
use super::controls::{level_meter, shell_switch, status_summary};
|
||||||
use super::{Menu, MenuRows, MenuStyle};
|
use super::{Menu, MenuRows, MenuStyle};
|
||||||
use crate::status::Action;
|
use crate::status::Action;
|
||||||
use crate::{FereseShell, color_with_opacity, text};
|
use crate::{FereseShell, text};
|
||||||
|
|
||||||
pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: MenuStyle) -> MenuRows<'a> {
|
pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: MenuStyle) -> MenuRows<'a> {
|
||||||
let MenuStyle {
|
let MenuStyle {
|
||||||
theme,
|
primary, opacity: p, ..
|
||||||
primary,
|
|
||||||
muted,
|
|
||||||
opacity: p,
|
|
||||||
} = style;
|
} = style;
|
||||||
if let Some(n) = &shell.status.network {
|
if let Some(n) = &shell.status.network {
|
||||||
let connected = n.enabled && n.connection.is_some();
|
let connected = n.enabled && n.connection.is_some();
|
||||||
@@ -30,10 +27,7 @@ pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: Me
|
|||||||
} else {
|
} else {
|
||||||
"Wireless connections are paused"
|
"Wireless connections are paused"
|
||||||
},
|
},
|
||||||
primary,
|
style,
|
||||||
muted,
|
|
||||||
color_with_opacity(theme.accent, p),
|
|
||||||
p,
|
|
||||||
n.enabled,
|
n.enabled,
|
||||||
None,
|
None,
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -178,21 +178,6 @@ fn connection_control<'a>(
|
|||||||
container(tile).width(Length::FillPortion(1)).into()
|
container(tile).width(Length::FillPortion(1)).into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
#[test]
|
|
||||||
fn connection_captions_fit_one_short_line() {
|
|
||||||
assert_eq!(super::connection_caption("88%"), "88%");
|
|
||||||
assert_eq!(super::connection_caption("Flow84@Lofree"), "Flow84@Lofr…");
|
|
||||||
assert_eq!(super::connection_caption("123456789012"), "123456789012");
|
|
||||||
assert_eq!(super::connection_caption("My very long headphones"), "My very lon…");
|
|
||||||
assert_eq!(super::connection_caption(" Device\n name "), "Device name");
|
|
||||||
let caption = super::connection_caption("耳機耳機耳機耳機耳機耳機耳機");
|
|
||||||
assert_eq!(caption.chars().count(), 12);
|
|
||||||
assert!(caption.ends_with('…'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn heading<'a>(
|
pub(super) fn heading<'a>(
|
||||||
shell: &'a FereseShell,
|
shell: &'a FereseShell,
|
||||||
mut heading: super::MenuHeading<'a>,
|
mut heading: super::MenuHeading<'a>,
|
||||||
@@ -229,3 +214,18 @@ pub(super) fn heading<'a>(
|
|||||||
}
|
}
|
||||||
heading
|
heading
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn connection_captions_fit_one_short_line() {
|
||||||
|
assert_eq!(super::connection_caption("88%"), "88%");
|
||||||
|
assert_eq!(super::connection_caption("Flow84@Lofree"), "Flow84@Lofr…");
|
||||||
|
assert_eq!(super::connection_caption("123456789012"), "123456789012");
|
||||||
|
assert_eq!(super::connection_caption("My very long headphones"), "My very lon…");
|
||||||
|
assert_eq!(super::connection_caption(" Device\n name "), "Device name");
|
||||||
|
let caption = super::connection_caption("耳機耳機耳機耳機耳機耳機耳機");
|
||||||
|
assert_eq!(caption.chars().count(), 12);
|
||||||
|
assert!(caption.ends_with('…'));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user