diff --git a/compositor/src/backends/direct.rs b/compositor/src/backends/direct.rs index 0907d0f..e334103 100644 --- a/compositor/src/backends/direct.rs +++ b/compositor/src/backends/direct.rs @@ -454,13 +454,9 @@ fn open_primary_device( .connected_outputs = scan.connected_outputs; let mut outputs = HashMap::new(); for selection in scan.selections { - let OutputSelection { - connector, - crtc, - mode, - settings, - } = selection; - let output = create_direct_output(state, &mut drm, &gbm, &renderer, connector, crtc, mode, &settings)?; + let crtc = selection.crtc; + let mode = selection.mode; + let output = create_direct_output(state, &mut drm, &gbm, &renderer, selection)?; state .direct_backend .as_mut() @@ -959,22 +955,9 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) { } for (_, selection) in selections { - let OutputSelection { - connector, - crtc, - mode, - settings, - } = selection; - match create_direct_output( - state, - &mut device.drm, - &device.gbm, - &device.renderer, - connector, - crtc, - mode, - &settings, - ) { + let crtc = selection.crtc; + let mode = selection.mode; + match create_direct_output(state, &mut device.drm, &device.gbm, &device.renderer, selection) { Ok(output) => { state .direct_backend @@ -1419,11 +1402,14 @@ fn create_direct_output( drm: &mut DrmDevice, gbm: &GbmDevice, renderer: &GlesRenderer, - connector: connector::Info, - crtc: crtc::Handle, - mode: DrmMode, - settings: &OutputSettings, + selection: OutputSelection, ) -> Result> { + let OutputSelection { + connector, + crtc, + mode, + settings, + } = selection; let identity = connector_identity(drm, &connector); let drm_surface = drm.create_surface(crtc, mode, &[connector.handle()])?; 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 // 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 render_metrics = RenderMetrics::from_environment(output.name()); diff --git a/compositor/src/config.rs b/compositor/src/config.rs index da28a06..e2a4c52 100644 --- a/compositor/src/config.rs +++ b/compositor/src/config.rs @@ -96,22 +96,22 @@ impl Config { theme: &ferese_config::theme::ResolvedTheme, inactive_dim: InactiveDimSettings, ) -> Result { - let mut config = Self::default(); - config.theme = serde_json::from_value(serde_json::to_value(&theme.tokens).unwrap()) - .expect("resolved theme matches runtime schema"); + let config = Self { + theme: serde_json::from_value(serde_json::to_value(&theme.tokens).unwrap()) + .expect("resolved theme matches runtime schema"), + ..Self::default() + }; let mut settings = config.theme_settings()?; settings.inactive_dim = inactive_dim; Ok(settings) } pub(crate) fn runtime_config(&self) -> Result { - self.notifications.validate().map_err(ConfigError::InvalidBinding)?; - self.desktop_widgets.validate().map_err(ConfigError::InvalidBinding)?; + self.notifications.validate().map_err(ConfigError::Binding)?; + self.desktop_widgets.validate().map_err(ConfigError::Binding)?; for daemon in &self.autostart { if daemon.command.first().is_none_or(|program| program.trim().is_empty()) { - return Err(ConfigError::InvalidBinding( - "autostart command must contain a program".into(), - )); + return Err(ConfigError::Binding("autostart command must contain a program".into())); } } let input_settings = self.input_settings()?; @@ -217,29 +217,29 @@ pub enum ConfigError { path: PathBuf, source: ferese_config::Error, }, - InvalidColumnWidth { + ColumnWidth { field: &'static str, value: String, }, - InvalidAnimationValue { + AnimationValue { field: &'static str, value: f64, }, - InvalidLayoutValue { + LayoutValue { field: &'static str, value: f64, }, - InvalidInputValue { + InputValue { field: &'static str, value: String, }, - InvalidBinding(String), - InvalidWindowRule(String), - InvalidThemeValue { + Binding(String), + WindowRule(String), + ThemeValue { field: &'static str, value: String, }, - InvalidOutputProfile(String), + OutputProfile(String), } impl fmt::Display for ConfigError { @@ -249,25 +249,25 @@ impl fmt::Display for ConfigError { Self::Parse { path, source } => { write!(formatter, "failed to parse {}: {source}", path.display()) } - Self::InvalidColumnWidth { field, value } => write!( + Self::ColumnWidth { field, value } => write!( formatter, "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}") } - Self::InvalidLayoutValue { field, value } => { + Self::LayoutValue { field, value } => { write!(formatter, "invalid layout.{field} value {value}") } - Self::InvalidInputValue { field, value } => { + Self::InputValue { field, value } => { write!(formatter, "invalid input.{field} value {value}") } - Self::InvalidBinding(message) => write!(formatter, "invalid binding: {message}"), - Self::InvalidWindowRule(message) => write!(formatter, "invalid window rule: {message}"), - Self::InvalidThemeValue { field, value } => { + Self::Binding(message) => write!(formatter, "invalid binding: {message}"), + Self::WindowRule(message) => write!(formatter, "invalid window rule: {message}"), + Self::ThemeValue { field, value } => { write!(formatter, "invalid theme.{field} value {value}") } - Self::InvalidOutputProfile(message) => { + Self::OutputProfile(message) => { write!(formatter, "invalid output profile: {message}") } } @@ -279,21 +279,21 @@ impl Error for ConfigError { match self { #[cfg(test)] Self::Parse { source, .. } => Some(source), - Self::InvalidColumnWidth { .. } - | Self::InvalidAnimationValue { .. } - | Self::InvalidLayoutValue { .. } - | Self::InvalidInputValue { .. } - | Self::InvalidBinding(_) - | Self::InvalidWindowRule(_) - | Self::InvalidThemeValue { .. } - | Self::InvalidOutputProfile(_) => None, + Self::ColumnWidth { .. } + | Self::AnimationValue { .. } + | Self::LayoutValue { .. } + | Self::InputValue { .. } + | Self::Binding(_) + | Self::WindowRule(_) + | Self::ThemeValue { .. } + | Self::OutputProfile(_) => None, } } } impl Config { pub fn window_rules(&self) -> Result, ConfigError> { - window_rules::validate(&self.window_rules).map_err(ConfigError::InvalidWindowRule) + window_rules::validate(&self.window_rules).map_err(ConfigError::WindowRule) } } diff --git a/compositor/src/config/appearance.rs b/compositor/src/config/appearance.rs index 6f52aca..044e9c7 100644 --- a/compositor/src/config/appearance.rs +++ b/compositor/src/config/appearance.rs @@ -64,7 +64,7 @@ pub(super) fn nonnegative_theme_value(value: f64, field: &'static str) -> Result if value.is_finite() && value >= 0.0 { Ok(value) } else { - Err(ConfigError::InvalidThemeValue { + Err(ConfigError::ThemeValue { field, value: value.to_string(), }) @@ -75,7 +75,7 @@ pub(super) fn finite_theme_value(value: f64, field: &'static str) -> Result Result Result Result { let digits = value.strip_prefix('#').unwrap_or(value); if !digits.is_ascii() || !matches!(digits.len(), 6 | 8) { - return Err(ConfigError::InvalidThemeValue { + return Err(ConfigError::ThemeValue { field, value: value.to_owned(), }); @@ -108,13 +108,13 @@ pub(super) fn parse_color(value: &str, field: &'static str) -> Result>) -> Result<(), ConfigError> { for (name, argv) in commands { 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() { - return Err(ConfigError::InvalidBinding(format!( - "command {name:?} must contain a program" - ))); + return Err(ConfigError::Binding(format!("command {name:?} must contain a program"))); } } @@ -74,7 +69,7 @@ pub(crate) fn physical_keymap(input: &InputSettings) -> Result Result { if configured.disabled { - return Err(ConfigError::InvalidBinding(format!( + return Err(ConfigError::Binding(format!( "disabled binding {:?} cannot be executed", configured.keys ))); @@ -93,7 +88,7 @@ pub(super) fn parse_binding( let action = configured .action .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 trigger = match identity.match_mode { BindingMatch::Keysym => BindingTrigger::Keysym(identity.key), @@ -123,7 +118,7 @@ pub(super) fn binding_identity( let (modifiers, key) = parse_chord(&configured.keys)?; if let Some((fingers, direction)) = parse_swipe(&key) { 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(), )); } @@ -139,13 +134,13 @@ pub(super) fn binding_identity( .key_by_name(&key.to_ascii_uppercase()) .map(Keycode::raw) .ok_or_else(|| { - ConfigError::InvalidBinding(format!( + ConfigError::Binding(format!( "unknown XKB physical key name {key:?} in {:?}", configured.keys )) })?, BindingMatch::Swipe => { - return Err(ConfigError::InvalidBinding( + return Err(ConfigError::Binding( "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) { 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() { @@ -190,17 +185,17 @@ pub(super) fn parse_chord(chord: &str) -> Result<(BindingModifiers, String), Con }; if let Some(slot) = slot { if *slot { - return Err(ConfigError::InvalidBinding(format!("duplicate modifier in {chord:?}"))); + return Err(ConfigError::Binding(format!("duplicate modifier in {chord:?}"))); } *slot = true; } 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" ))); } } - 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)) } @@ -236,7 +231,7 @@ pub(super) fn parse_keysym(name: &str) -> Result { symbol = xkb::keysym_from_name(&normalized, xkb::KEYSYM_CASE_INSENSITIVE); } if symbol.raw() == keysyms::KEY_NoSymbol { - Err(ConfigError::InvalidBinding(format!("unknown keysym {name:?}"))) + Err(ConfigError::Binding(format!("unknown keysym {name:?}"))) } else { Ok(symbol.raw()) } @@ -249,7 +244,7 @@ pub(super) fn parse_action( ) -> Result { let no_argument = || { if argument.is_some() { - Err(ConfigError::InvalidBinding(format!( + Err(ConfigError::Binding(format!( "action {action:?} does not accept an argument" ))) } else { @@ -257,7 +252,7 @@ pub(super) fn parse_action( } }; 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 { "none" => { @@ -276,7 +271,7 @@ pub(super) fn parse_action( let command = required_argument()?; let argv = commands .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())) } "close" => { @@ -332,7 +327,7 @@ pub(super) fn parse_action( no_argument()?; 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 "right" => Ok(Direction::Right), "up" => Ok(Direction::Up), "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 { match argument.parse() { 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 { pub(crate) fn portal_trigger(chord: &str) -> Result { 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::>(); if parts @@ -468,7 +463,7 @@ impl Binding { .count() > 1 { - return Err(ConfigError::InvalidBinding("Duplicate NUM modifier".into())); + return Err(ConfigError::Binding("Duplicate NUM modifier".into())); } let chord = parts .into_iter() @@ -482,7 +477,7 @@ impl Binding { && matches!(symbol, keysyms::KEY_Escape | keysyms::KEY_F1..=keysyms::KEY_F12)) || (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 { modifiers, diff --git a/compositor/src/config/input_motion.rs b/compositor/src/config/input_motion.rs index 0de1952..881bafd 100644 --- a/compositor/src/config/input_motion.rs +++ b/compositor/src/config/input_motion.rs @@ -3,26 +3,26 @@ use super::*; impl Config { pub fn input_settings(&self) -> Result { if self.input.xkb_layout.trim().is_empty() { - return Err(ConfigError::InvalidInputValue { + return Err(ConfigError::InputValue { field: "xkb_layout", value: self.input.xkb_layout.clone(), }); } if self.input.repeat_rate <= 0 { - return Err(ConfigError::InvalidInputValue { + return Err(ConfigError::InputValue { field: "repeat_rate", value: self.input.repeat_rate.to_string(), }); } if self.input.repeat_delay_ms < 0 { - return Err(ConfigError::InvalidInputValue { + return Err(ConfigError::InputValue { field: "repeat_delay_ms", value: self.input.repeat_delay_ms.to_string(), }); } if !(16..=1000).contains(&self.input.touchpad.swipe_threshold) { - return Err(ConfigError::InvalidInputValue { + return Err(ConfigError::InputValue { field: "touchpad.swipe_threshold", 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 damping = self.animations.spring.damping; if !damping.is_finite() || damping < 0.0 { - return Err(ConfigError::InvalidAnimationValue { + return Err(ConfigError::AnimationValue { field: "spring.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 { Ok(value) } else { - Err(ConfigError::InvalidAnimationValue { field, value }) + Err(ConfigError::AnimationValue { field, value }) } } diff --git a/compositor/src/config/layout.rs b/compositor/src/config/layout.rs index ee756da..ca0ecdd 100644 --- a/compositor/src/config/layout.rs +++ b/compositor/src/config/layout.rs @@ -59,7 +59,7 @@ pub(super) fn nonnegative_layout_value(value: f64, field: &'static str) -> Resul if value.is_finite() && value >= 0.0 { Ok(value) } 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 { 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), - value => Err(ConfigError::InvalidColumnWidth { + value => Err(ConfigError::ColumnWidth { field, value: match value { ColumnWidthValue::Proportion(value) => value.to_string(), diff --git a/compositor/src/config/outputs.rs b/compositor/src/config/outputs.rs index fde8a62..4c4fce9 100644 --- a/compositor/src/config/outputs.rs +++ b/compositor/src/config/outputs.rs @@ -9,19 +9,13 @@ impl Config { .map(|profile| { let name = profile.name.trim(); if name.is_empty() { - return Err(ConfigError::InvalidOutputProfile( - "profile names cannot be empty".to_owned(), - )); + return Err(ConfigError::OutputProfile("profile names cannot be empty".to_owned())); } if !names.insert(name.to_owned()) { - return Err(ConfigError::InvalidOutputProfile(format!( - "duplicate profile name {name:?}" - ))); + return Err(ConfigError::OutputProfile(format!("duplicate profile name {name:?}"))); } if profile.outputs.is_empty() { - return Err(ConfigError::InvalidOutputProfile(format!( - "profile {name:?} has no outputs" - ))); + return Err(ConfigError::OutputProfile(format!("profile {name:?} has no outputs"))); } let mut matchers = HashSet::new(); @@ -31,17 +25,17 @@ impl Config { .map(|output| { let matcher = output.matcher.trim(); if matcher.is_empty() { - return Err(ConfigError::InvalidOutputProfile(format!( + return Err(ConfigError::OutputProfile(format!( "profile {name:?} contains an empty output matcher" ))); } if !matchers.insert(matcher.to_owned()) { - return Err(ConfigError::InvalidOutputProfile(format!( + return Err(ConfigError::OutputProfile(format!( "profile {name:?} repeats output matcher {matcher:?}" ))); } 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 {}", output.scale ))); @@ -56,7 +50,7 @@ impl Config { .as_deref() .map(parse_output_mode) .transpose() - .map_err(ConfigError::InvalidOutputProfile)?, + .map_err(ConfigError::OutputProfile)?, scale: output.scale, transform: output.transform, position: output.position, diff --git a/compositor/src/input_capture.rs b/compositor/src/input_capture.rs index ccd9e4a..8d7be6a 100644 --- a/compositor/src/input_capture.rs +++ b/compositor/src/input_capture.rs @@ -215,10 +215,13 @@ impl InputCapture { } for barrier in barriers { let kind = edge(&barrier, &self.zones); - if zone_set != self.zone_set || counts[&barrier.id] != 1 || kind.is_none() { - failed.push(barrier.id); + if zone_set == self.zone_set + && counts[&barrier.id] == 1 + && let Some(kind) = kind + { + accepted.push((barrier, kind)); } else { - accepted.push((barrier, kind.unwrap())); + failed.push(barrier.id); } } 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, + to: smithay::utils::Point, + 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::>(); + 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)] mod tests { use super::*; @@ -556,103 +659,3 @@ mod tests { 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, - to: smithay::utils::Point, - 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::>(); - 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 - } -} diff --git a/compositor/src/render/capture.rs b/compositor/src/render/capture.rs index d6074ef..daf4ca7 100644 --- a/compositor/src/render/capture.rs +++ b/compositor/src/render/capture.rs @@ -104,11 +104,10 @@ pub(crate) fn capture_window_frame( pixel.swap(0, 2); let alpha = pixel[3] as u16; for channel in &mut pixel[..3] { - *channel = if alpha == 0 { - 0 - } else { - ((*channel as u16 * 255 + alpha / 2) / alpha).min(255) as u8 - }; + *channel = (*channel as u16 * 255 + alpha / 2) + .checked_div(alpha) + .unwrap_or(0) + .min(255) as u8; } } Ok(crate::handlers::screenshot::CaptureBuffer { diff --git a/compositor/src/render/materials.rs b/compositor/src/render/materials.rs index 28abb9d..a7e5b2c 100644 --- a/compositor/src/render/materials.rs +++ b/compositor/src/render/materials.rs @@ -37,7 +37,19 @@ pub(super) fn append_material_surface( .iter() .enumerate() .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(); 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, + pub radius: f32, + pub index: usize, + pub capture_geometry: Rectangle, + pub alpha: f32, +} + pub(super) fn material_element( state: &mut Ferese, renderer: &mut GlesRenderer, output: &Output, surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface, - geometry: Rectangle, - radius: f32, - index: usize, - capture_geometry: Rectangle, - alpha: f32, + surface_geometry: MaterialSurface, ) -> Option<(AnimatedWindowRenderElement, AnimatedWindowRenderElement)> { + let MaterialSurface { + geometry, + radius, + index, + capture_geometry, + alpha, + } = surface_geometry; let (role, generation) = crate::effects::surface_role(surface)?; let material = crate::effects::resolve_material( role, diff --git a/compositor/src/render/mod.rs b/compositor/src/render/mod.rs index f9a4518..0ef04d0 100644 --- a/compositor/src/render/mod.rs +++ b/compositor/src/render/mod.rs @@ -527,7 +527,7 @@ mod tests { .unwrap(); let size = (32, 32).into(); 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; } let blurred = renderer.import_memory(&pixels, Fourcc::Abgr8888, size, false).unwrap(); diff --git a/compositor/src/render/scene.rs b/compositor/src/render/scene.rs index a7af773..4473156 100644 --- a/compositor/src/render/scene.rs +++ b/compositor/src/render/scene.rs @@ -306,11 +306,13 @@ pub(crate) fn output_elements( renderer, output, surface, - constrain, - window_radius as f32, - 0, - constrain, - close_alpha, + MaterialSurface { + geometry: constrain, + radius: window_radius as f32, + index: 0, + capture_geometry: constrain, + alpha: close_alpha, + }, ) { elements.push(background); diff --git a/compositor/src/session_lock.rs b/compositor/src/session_lock.rs index 03051b2..0435c03 100644 --- a/compositor/src/session_lock.rs +++ b/compositor/src/session_lock.rs @@ -36,7 +36,7 @@ impl Default for IdleSettings { impl IdleSettings { pub(crate) fn validate(self) -> Result { 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", value: "must be between 0 and 86400 seconds".into(), }); diff --git a/crates/ferese-config/src/theme.rs b/crates/ferese-config/src/theme.rs index 9ffb0c5..f3be80c 100644 --- a/crates/ferese-config/src/theme.rs +++ b/crates/ferese-config/src/theme.rs @@ -1248,6 +1248,53 @@ impl ResolvedTheme { } } +pub type ImportedFamily = (crate::families::Family, (Option, Option), Vec); + +pub fn import_family(id: &str, source: &str) -> Result { + 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)] mod tests { use super::*; @@ -1697,51 +1744,3 @@ mod tests { assert_eq!(ResolvedTheme::default().transition(&light, 0.), light); } } - -pub fn import_family( - id: &str, - source: &str, -) -> Result<(crate::families::Family, (Option, Option), Vec), 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)) -} diff --git a/crates/ferese-theme/src/gallery.rs b/crates/ferese-theme/src/gallery.rs index e9246d3..903449e 100644 --- a/crates/ferese-theme/src/gallery.rs +++ b/crates/ferese-theme/src/gallery.rs @@ -6,17 +6,29 @@ use ferese_config::families::{Family, Palette}; use ferese_config::theme::Appearance; use iced_accessibility::{A11yTree, accesskit}; +pub struct TileOptions { + pub variant: Option, + pub active: Option, + pub selected: bool, + pub palette: crate::Palette, + pub font: cosmic::font::Font, + pub id: Id, +} + pub fn tile( family: &Family, - variant: Option, - active: Option, - selected: bool, - palette: crate::Palette, - font: cosmic::font::Font, - id: Id, + options: TileOptions, on_select: M, navigate: impl Fn(&keyboard::key::Named) -> Option + 'static, ) -> Element<'static, M> { + let TileOptions { + variant, + active, + selected, + palette, + font, + id, + } = options; use cosmic::widget::{button, column, row}; let mut caption = row([]) .spacing(6) @@ -225,13 +237,15 @@ impl cosmic::iced::advanced::widget::operation::Focusable for Focus { } } +type Navigation<'a, M> = Box Option + 'a>; + pub struct Radio<'a, M> { content: Element<'a, M>, id: Id, selected: bool, group: bool, label: String, - navigate: Option Option + 'a>>, + navigate: Option>, } pub fn radio<'a, M: Clone + 'a>( diff --git a/crates/ferese-theme/src/palette.rs b/crates/ferese-theme/src/palette.rs index 7b25f3d..1ac8a32 100644 --- a/crates/ferese-theme/src/palette.rs +++ b/crates/ferese-theme/src/palette.rs @@ -120,8 +120,10 @@ mod tests { ferese_config::theme::Appearance::Dark, ] { for high_contrast in [false, true] { - let mut resolved = ferese_config::theme::ResolvedTheme::default(); - resolved.appearance = appearance; + let mut resolved = ferese_config::theme::ResolvedTheme { + appearance, + ..Default::default() + }; resolved.accessibility.increase_contrast = high_contrast; let palette = Palette::from_resolved(&resolved); assert_eq!(palette.high_contrast, high_contrast); @@ -133,9 +135,11 @@ mod tests { #[test] fn every_preset_builds_an_opaque_control_palette_over_translucent_materials() { for preset in crate::PRESETS { - let mut resolved = ferese_config::theme::ResolvedTheme::default(); - resolved.appearance = preset.appearance; - resolved.tokens = ferese_config::theme::preset(preset.id, preset.appearance).unwrap(); + let mut resolved = ferese_config::theme::ResolvedTheme { + appearance: preset.appearance, + tokens: ferese_config::theme::preset(preset.id, preset.appearance).unwrap(), + ..Default::default() + }; resolved.tokens.material.style = "translucent".into(); resolved.tokens.material.opacity = 0.25; resolved.tokens.geometry.shell_radius = 0.; diff --git a/portal/src/backend.rs b/portal/src/backend.rs index b3f2233..e3d4ad7 100644 --- a/portal/src/backend.rs +++ b/portal/src/backend.rs @@ -393,12 +393,14 @@ impl Backend { let sharing = start_streams( &app_id, &parent_window, - multiple, - cursor, - source_types, - persist_mode, - restore, - bar_controlled, + StreamOptions { + multiple, + cursor, + source_types, + requested_persistence: persist_mode, + restore, + bar_controlled, + }, ); let result = tokio::select! { _ = cancel.wait() => Err(StartError::Cancelled), @@ -534,16 +536,28 @@ async fn picker(prompt: &Prompt) -> Result { Ok(child) } -async fn start_streams( - app: &str, - parent: &str, +struct StreamOptions { multiple: bool, cursor: bool, source_types: u32, requested_persistence: u32, restore: Option, bar_controlled: bool, +} + +async fn start_streams( + app: &str, + parent: &str, + options: StreamOptions, ) -> Result<(Vec, 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))) .await .map_err(|e| e.to_string())??; diff --git a/portal/src/consent.rs b/portal/src/consent.rs index f8b4a2c..9859cc0 100644 --- a/portal/src/consent.rs +++ b/portal/src/consent.rs @@ -29,9 +29,9 @@ pub(crate) struct ShortcutField { #[derive(Clone, Debug)] enum Message { - ThemeChanged(ferese_config::theme::Snapshot), + ThemeChanged(Box), Opened(cosmic::iced::window::Id), - Attached(Result<(Option>, Result), String>), + Attached(crate::parent::Attachment), Accept, Cancel, Shortcut(usize, String), @@ -110,7 +110,7 @@ impl cosmic::Application for Consent { fn subscription(&self) -> cosmic::iced::Subscription { 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::Window(cosmic::iced::window::Event::Opened { .. }) => Some(Message::Opened(id)), cosmic::iced::Event::Keyboard(cosmic::iced::keyboard::Event::KeyPressed { diff --git a/portal/src/desktop.rs b/portal/src/desktop.rs index 8bc54bb..a5f48ee 100644 --- a/portal/src/desktop.rs +++ b/portal/src/desktop.rs @@ -21,8 +21,10 @@ const PATH: &str = "/org/freedesktop/portal/desktop"; const MAX_IMAGE_BYTES: u64 = 128 * 1024 * 1024; type Reply = (u32, Options); +type RequestMap = HashMap)>; + #[derive(Clone)] -pub(crate) struct Requests(Arc)>>>, usize); +pub(crate) struct Requests(Arc>, usize); impl Default for Requests { fn default() -> Self { @@ -141,6 +143,10 @@ pub(crate) struct Wallpaper(pub(crate) Requests); #[zbus::interface(name = "org.freedesktop.impl.portal.Wallpaper")] impl Wallpaper { #[zbus(name = "SetWallpaperURI")] + #[allow( + clippy::too_many_arguments, + reason = "Signature is defined by the portal D-Bus interface" + )] async fn set_wallpaper_uri( &self, handle: OwnedObjectPath, diff --git a/portal/src/inhibit.rs b/portal/src/inhibit.rs index 953db2d..eed125c 100644 --- a/portal/src/inhibit.rs +++ b/portal/src/inhibit.rs @@ -258,6 +258,10 @@ async fn login_inhibitor(what: &str, app: &str, reason: &str) -> Result>, + Result, + ), + String, +>; + use wayland_client::globals::{GlobalListContents, registry_queue_init}; use wayland_client::protocol::{wl_registry, wl_surface}; use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, delegate_noop}; diff --git a/portal/src/permissions.rs b/portal/src/permissions.rs index 1d7b843..d2609b3 100644 --- a/portal/src/permissions.rs +++ b/portal/src/permissions.rs @@ -24,6 +24,10 @@ impl Usb { 1 } + #[allow( + clippy::too_many_arguments, + reason = "Signature is defined by the portal D-Bus interface" + )] async fn acquire_devices( &self, handle: OwnedObjectPath, diff --git a/portal/src/picker.rs b/portal/src/picker.rs index dc7119f..f737b42 100644 --- a/portal/src/picker.rs +++ b/portal/src/picker.rs @@ -52,17 +52,9 @@ pub(crate) struct Selection { #[derive(Clone, Debug)] enum Message { - ThemeChanged(ferese_config::theme::Snapshot), + ThemeChanged(Box), WindowOpened(cosmic::iced::window::Id), - Attached( - Result< - ( - Option>, - Result, - ), - String, - >, - ), + Attached(crate::parent::Attachment), Select(usize), Remember, Share, @@ -161,7 +153,7 @@ impl cosmic::Application for Picker { fn subscription(&self) -> cosmic::iced::Subscription { 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::Window(cosmic::iced::window::Event::Opened { .. }) => { Some(Message::WindowOpened(id)) diff --git a/portal/src/settings.rs b/portal/src/settings.rs index edc62d3..2600b18 100644 --- a/portal/src/settings.rs +++ b/portal/src/settings.rs @@ -282,8 +282,10 @@ mod tests { #[test] fn reports_ferese_theme_and_reduced_motion() { - let mut resolved = ResolvedTheme::default(); - resolved.appearance = ThemeAppearance::Light; + let mut resolved = ResolvedTheme { + appearance: ThemeAppearance::Light, + ..Default::default() + }; resolved.tokens.colors.accent = "#ff8000".into(); resolved.reduced_motion = true; resolved.accessibility.increase_contrast = true; diff --git a/portal/src/shortcuts.rs b/portal/src/shortcuts.rs index ae89d4b..5970e18 100644 --- a/portal/src/shortcuts.rs +++ b/portal/src/shortcuts.rs @@ -310,6 +310,10 @@ impl GlobalShortcuts { result } + #[allow( + clippy::too_many_arguments, + reason = "Signature is defined by the portal D-Bus interface" + )] async fn bind_shortcuts( &self, handle: OwnedObjectPath, diff --git a/shell/ferese-lock/src/appearance.rs b/shell/ferese-lock/src/appearance.rs index 4e89344..1116ca5 100644 --- a/shell/ferese-lock/src/appearance.rs +++ b/shell/ferese-lock/src/appearance.rs @@ -169,6 +169,35 @@ fn account_picture(user: &str) -> Option { }) } +pub fn load_wallpaper(path: &std::path::Path, blur: f32) -> Result { + 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)] mod tests { use super::*; @@ -215,39 +244,10 @@ mod tests { appearance.accent, 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_eq!(appearance.clock_format(), "%I:%M %p"); assert_eq!(appearance.dim, 0.7); assert_eq!(appearance.theme().cosmic().corner_radii.radius_m, [0.; 4]); } } - -pub fn load_wallpaper(path: &std::path::Path, blur: f32) -> Result { - 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(), - )) -} diff --git a/shell/ferese-lock/src/main.rs b/shell/ferese-lock/src/main.rs index e98625d..7e31125 100644 --- a/shell/ferese-lock/src/main.rs +++ b/shell/ferese-lock/src/main.rs @@ -82,7 +82,7 @@ fn main() -> iced::Result { #[derive(Clone)] enum Message { - ThemeChanged(ferese_config::theme::Snapshot), + ThemeChanged(Box), WallpaperLoaded(u64, Result), Event(Box), Input(String), @@ -193,7 +193,7 @@ impl cosmic::Application for Locker { if self.preview { Subscription::none() } 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::PlatformSpecific(PlatformSpecific::Wayland( diff --git a/shell/ferese-settings/src/main.rs b/shell/ferese-settings/src/main.rs index c40ae87..2dd1e09 100644 --- a/shell/ferese-settings/src/main.rs +++ b/shell/ferese-settings/src/main.rs @@ -52,7 +52,7 @@ enum Message { RefreshDisplays, DisplaysLoaded(Result, String>), RefreshRate(String, displays::Mode, bool), - ThemeChanged(ferese_config::theme::Snapshot), + ThemeChanged(Box), DragWindow, Page(Page), Search(String), @@ -207,7 +207,7 @@ impl cosmic::Application for App { return self.edit_many(displays::edits(&prefix, mode, automatic)); } Message::ThemeChanged(snapshot) => { - self.resolved = snapshot; + self.resolved = *snapshot; self.font = ferese_theme::font(Some(&self.resolved.presented.tokens.typography.font_family)); return self.update_theme(); } @@ -596,8 +596,8 @@ impl cosmic::Application for App { fn subscription(&self) -> cosmic::iced::Subscription { cosmic::iced::Subscription::batch([ - cosmic::iced::Subscription::run_with(self.path.clone(), watch::changes), - ferese_theme::service::subscription().map(Message::ThemeChanged), + cosmic::iced::Subscription::run_with(self.path.clone(), |path| watch::changes(path)), + ferese_theme::service::subscription().map(|snapshot| Message::ThemeChanged(Box::new(snapshot))), if self.page == Page::Displays { cosmic::iced::time::every(std::time::Duration::from_secs(2)).map(|_| Message::RefreshDisplays) } else { diff --git a/shell/ferese-settings/src/pages.rs b/shell/ferese-settings/src/pages.rs index 3a91cda..b76c841 100644 --- a/shell/ferese-settings/src/pages.rs +++ b/shell/ferese-settings/src/pages.rs @@ -412,12 +412,14 @@ impl App { format!("{prefix}.scale"), "Scale", "Logical size of text and controls.", - 1., - 0.75, - 3., - 0.25, - "×", - false, + schema::RangeSpec { + default: 1., + min: 0.75, + max: 3., + step: 0.25, + suffix: "×", + integer: false, + }, ))); body = body.push(container(group).padding(8).class(visuals::surface(palette.card, 14.))); } diff --git a/shell/ferese-settings/src/schema.rs b/shell/ferese-settings/src/schema.rs index 72080d6..ecaa933 100644 --- a/shell/ferese-settings/src/schema.rs +++ b/shell/ferese-settings/src/schema.rs @@ -148,17 +148,30 @@ fn toggle(path: &str, label: &str, description: &str, default: bool) -> Field { 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( path: impl Into, label: impl Into, description: impl Into, - default: f64, - min: f64, - max: f64, - step: f64, - suffix: &'static str, - integer: bool, + spec: RangeSpec, ) -> Field { + let RangeSpec { + default, + min, + max, + step, + suffix, + integer, + } = spec; Field::new( path, label, @@ -278,100 +291,118 @@ fn clock_fields() -> Vec { "desktop_widgets.clock.margin_x", "Horizontal margin", "Distance from left/right edge; ignored when horizontally centered.", - 64.0, - 0.0, - 1000.0, - 1.0, - " px", - true, + RangeSpec { + default: 64.0, + min: 0.0, + max: 1000.0, + step: 1.0, + suffix: " px", + integer: true, + }, ), range( "desktop_widgets.clock.margin_y", "Vertical margin", "Distance from top/bottom edge; ignored when vertically centered.", - 80.0, - 0.0, - 1000.0, - 1.0, - " px", - true, + RangeSpec { + default: 80.0, + min: 0.0, + max: 1000.0, + step: 1.0, + suffix: " px", + integer: true, + }, ), range( "desktop_widgets.clock.width", "Widget width", "Logical pixels; increase for longer formats.", - 440.0, - 64.0, - 1600.0, - 1.0, - " px", - true, + RangeSpec { + default: 440.0, + min: 64.0, + max: 1600.0, + step: 1.0, + suffix: " px", + integer: true, + }, ), range( "desktop_widgets.clock.height", "Widget height", "Leave room for both text lines.", - 320.0, - 32.0, - 800.0, - 1.0, - " px", - true, + RangeSpec { + default: 320.0, + min: 32.0, + max: 800.0, + step: 1.0, + suffix: " px", + integer: true, + }, ), range( "desktop_widgets.clock.time_size", "Time size", "Logical pixels; scales with your output.", - 128.0, - 8.0, - 240.0, - 1.0, - " px", - false, + RangeSpec { + default: 128.0, + min: 8.0, + max: 240.0, + step: 1.0, + suffix: " px", + integer: false, + }, ), range( "desktop_widgets.clock.date_size", "Date size", "Separate size for the date line.", - 18.0, - 8.0, - 96.0, - 1.0, - " px", - false, + RangeSpec { + default: 18.0, + min: 8.0, + max: 96.0, + step: 1.0, + suffix: " px", + integer: false, + }, ), range( "desktop_widgets.clock.opacity", "Opacity", "Applies to text and the optional background.", - 0.9, - 0.0, - 1.0, - 0.05, - "", - false, + RangeSpec { + default: 0.9, + min: 0.0, + max: 1.0, + step: 0.05, + suffix: "", + integer: false, + }, ), range( "desktop_widgets.clock.gap", "Line spacing", "Space between time and date.", - 4.0, - 0.0, - 64.0, - 1.0, - " px", - false, + RangeSpec { + default: 4.0, + min: 0.0, + max: 64.0, + step: 1.0, + suffix: " px", + integer: false, + }, ), range( "desktop_widgets.clock.padding", "Inner padding", "Space around the labels.", - 12.0, - 0.0, - 64.0, - 1.0, - " px", - false, + RangeSpec { + default: 12.0, + min: 0.0, + max: 64.0, + step: 1.0, + suffix: " px", + integer: false, + }, ), choice( "desktop_widgets.clock.alignment", @@ -422,100 +453,118 @@ pub fn note_fields(index: usize) -> Vec { format!("{prefix}.width"), "Width", "Logical pixels; centered axes ignore margins.", - 320.0, - 120.0, - 1200.0, - 1.0, - " px", - true, + RangeSpec { + default: 320.0, + min: 120.0, + max: 1200.0, + step: 1.0, + suffix: " px", + integer: true, + }, ), range( format!("{prefix}.height"), "Height", "Logical pixels; centered axes ignore margins.", - 240.0, - 80.0, - 1200.0, - 1.0, - " px", - true, + RangeSpec { + default: 240.0, + min: 80.0, + max: 1200.0, + step: 1.0, + suffix: " px", + integer: true, + }, ), range( format!("{prefix}.margin_x"), "Horizontal margin", "Logical pixels; centered axes ignore margins.", - 48.0, - 0.0, - 1000.0, - 1.0, - " px", - true, + RangeSpec { + default: 48.0, + min: 0.0, + max: 1000.0, + step: 1.0, + suffix: " px", + integer: true, + }, ), range( format!("{prefix}.margin_y"), "Vertical margin", "Logical pixels; centered axes ignore margins.", - 80.0, - 0.0, - 1000.0, - 1.0, - " px", - true, + RangeSpec { + default: 80.0, + min: 0.0, + max: 1000.0, + step: 1.0, + suffix: " px", + integer: true, + }, ), range( format!("{prefix}.text_size"), "Text size", "Logical pixels; centered axes ignore margins.", - 16.0, - 8.0, - 96.0, - 1.0, - " px", - false, + RangeSpec { + default: 16.0, + min: 8.0, + max: 96.0, + step: 1.0, + suffix: " px", + integer: false, + }, ), range( format!("{prefix}.title_size"), "Title size", "Logical pixels; centered axes ignore margins.", - 18.0, - 8.0, - 96.0, - 1.0, - " px", - false, + RangeSpec { + default: 18.0, + min: 8.0, + max: 96.0, + step: 1.0, + suffix: " px", + integer: false, + }, ), range( format!("{prefix}.opacity"), "Opacity", "Logical pixels; centered axes ignore margins.", - 0.9, - 0.0, - 1.0, - 0.05, - "", - false, + RangeSpec { + default: 0.9, + min: 0.0, + max: 1.0, + step: 0.05, + suffix: "", + integer: false, + }, ), range( format!("{prefix}.padding"), "Padding", "Logical pixels; centered axes ignore margins.", - 20.0, - 0.0, - 64.0, - 1.0, - " px", - false, + RangeSpec { + default: 20.0, + min: 0.0, + max: 64.0, + step: 1.0, + suffix: " px", + integer: false, + }, ), range( format!("{prefix}.gap"), "Title spacing", "Logical pixels; centered axes ignore margins.", - 8.0, - 0.0, - 64.0, - 1.0, - " px", - false, + RangeSpec { + default: 8.0, + min: 0.0, + max: 64.0, + step: 1.0, + suffix: " px", + integer: false, + }, ), Field::new( format!("{prefix}.alignment"), @@ -536,23 +585,27 @@ pub fn fields(page: Page) -> Vec { "lock_screen.dim_after_seconds", "Dim after", "Dim the locked display after inactivity. Zero disables dimming.", - 30.0, - 0.0, - 600.0, - 5.0, - "s", - true, + RangeSpec { + default: 30.0, + min: 0.0, + max: 600.0, + step: 5.0, + suffix: "s", + integer: true, + }, ), range( "lock_screen.sleep_after_seconds", "Display sleep after", "Turn off locked displays after inactivity. Input wakes them; zero disables display sleep.", - 120.0, - 0.0, - 1800.0, - 15.0, - "s", - true, + RangeSpec { + default: 120.0, + min: 0.0, + max: 1800.0, + step: 15.0, + suffix: "s", + integer: true, + }, ), toggle( "lock_screen.show_clock", @@ -577,23 +630,27 @@ pub fn fields(page: Page) -> Vec { "lock_screen.background_blur", "Wallpaper softness", "Blur the lock screen wallpaper. Zero keeps the original image sharp.", - 18.0, - 0.0, - 40.0, - 1.0, - "", - false, + RangeSpec { + default: 18.0, + min: 0.0, + max: 40.0, + step: 1.0, + suffix: "", + integer: false, + }, ), range( "lock_screen.background_dim", "Wallpaper dimming", "Darken the wallpaper behind the clock and sign-in card.", - 0.48, - 0.0, - 0.9, - 0.02, - "", - false, + RangeSpec { + default: 0.48, + min: 0.0, + max: 0.9, + step: 0.02, + suffix: "", + integer: false, + }, ), ], Page::Accessibility => vec![ @@ -664,12 +721,14 @@ pub fn fields(page: Page) -> Vec { "theme.geometry.shell_radius", "Shell corner radius", "Rounds the bar, menus, notifications, desktop widgets and hover backgrounds. Window corners are separate.", - 14.0, - 0.0, - 32.0, - 1.0, - " px", - false, + RangeSpec { + default: 14.0, + min: 0.0, + max: 32.0, + step: 1.0, + suffix: " px", + integer: false, + }, ), text( "theme.accent", @@ -688,23 +747,27 @@ pub fn fields(page: Page) -> Vec { "theme.material.opacity", "Shell opacity", "Background opacity for the bar, popovers, menus, notifications and sharing dialog. Text and icons stay opaque.", - ferese_config::DEFAULT_MATERIAL_OPACITY, - 0.0, - 1.0, - 0.01, - "", - false, + RangeSpec { + default: ferese_config::DEFAULT_MATERIAL_OPACITY, + min: 0.0, + max: 1.0, + step: 0.01, + suffix: "", + integer: false, + }, ), range( "theme.material.blur_radius", "Background blur", "Softness behind translucent surfaces.", - 12.0, - 0.0, - 32.0, - 1.0, - " px", - false, + RangeSpec { + default: 12.0, + min: 0.0, + max: 32.0, + step: 1.0, + suffix: " px", + integer: false, + }, ), text( "theme.typography.font_family", @@ -716,12 +779,14 @@ pub fn fields(page: Page) -> Vec { "theme.geometry.focus_ring_width", "Focus border", "A subtle outline around the active window.", - 2.0, - 0.0, - 4.0, - 0.5, - " px", - false, + RangeSpec { + default: 2.0, + min: 0.0, + max: 4.0, + step: 0.5, + suffix: " px", + integer: false, + }, ), ], Page::Wallpaper => vec![ @@ -757,12 +822,14 @@ pub fn fields(page: Page) -> Vec { "notifications.timeout_ms", "Popup timeout", "Default duration; apps can request another timeout. Pauses while hovered.", - 6000.0, - 1000.0, - 30000.0, - 1000.0, - " ms", - true, + RangeSpec { + default: 6000.0, + min: 1000.0, + max: 30000.0, + step: 1000.0, + suffix: " ms", + integer: true, + }, ), ], Page::Bar => vec![ @@ -776,45 +843,53 @@ pub fn fields(page: Page) -> Vec { "theme.geometry.top_bar_height", "Height", "Keeps icon and text sizes unchanged.", - 28.0, - 24.0, - 48.0, - 1.0, - " px", - false, + RangeSpec { + default: 28.0, + min: 24.0, + max: 48.0, + step: 1.0, + suffix: " px", + integer: false, + }, ), range( "theme.geometry.top_bar_margin_top", "Top margin", "Set to zero for an edge-to-edge bar.", - 0.0, - 0.0, - 24.0, - 1.0, - " px", - true, + RangeSpec { + default: 0.0, + min: 0.0, + max: 24.0, + step: 1.0, + suffix: " px", + integer: true, + }, ), range( "theme.geometry.top_bar_margin_horizontal", "Side margins", "Space between the bar and the display edges.", - 0.0, - 0.0, - 32.0, - 1.0, - " px", - true, + RangeSpec { + default: 0.0, + min: 0.0, + max: 32.0, + step: 1.0, + suffix: " px", + integer: true, + }, ), range( "theme.geometry.top_bar_window_gap", "Window clearance", "Space below the menu bar.", - 0.0, - 0.0, - 24.0, - 1.0, - " px", - true, + RangeSpec { + default: 0.0, + min: 0.0, + max: 24.0, + step: 1.0, + suffix: " px", + integer: true, + }, ), toggle( "status.battery_percentage", @@ -852,45 +927,53 @@ pub fn fields(page: Page) -> Vec { "scrolling.default_column_width", "New window width", "Fraction of the available workspace.", - 0.5, - 0.25, - 1.0, - 0.05, - "", - false, + RangeSpec { + default: 0.5, + min: 0.25, + max: 1.0, + step: 0.05, + suffix: "", + integer: false, + }, ), range( "layout.inner_gap", "Between windows", "Space between neighboring windows.", - 10.0, - 0.0, - 32.0, - 1.0, - " px", - false, + RangeSpec { + default: 10.0, + min: 0.0, + max: 32.0, + step: 1.0, + suffix: " px", + integer: false, + }, ), range( "layout.outer_gap", "Workspace edges", "Space around the outside of the layout.", - 4.0, - 0.0, - 32.0, - 1.0, - " px", - false, + RangeSpec { + default: 4.0, + min: 0.0, + max: 32.0, + step: 1.0, + suffix: " px", + integer: false, + }, ), range( "theme.geometry.window_radius", "Corner radius", "Rounded corners for application windows.", - 14.0, - 0.0, - 28.0, - 1.0, - " px", - false, + RangeSpec { + default: 14.0, + min: 0.0, + max: 28.0, + step: 1.0, + suffix: " px", + integer: false, + }, ), toggle( "appearance.inactive_dim.enabled", @@ -902,12 +985,14 @@ pub fn fields(page: Page) -> Vec { "appearance.inactive_dim.amount", "Dimming amount", "How much inactive windows recede.", - 0.15, - 0.0, - 0.5, - 0.01, - "", - false, + RangeSpec { + default: 0.15, + min: 0.0, + max: 0.5, + step: 0.01, + suffix: "", + integer: false, + }, ), ], Page::Motion => vec![ @@ -921,23 +1006,27 @@ pub fn fields(page: Page) -> Vec { "animations.speed", "Animation speed", "Lower values give transitions more time.", - 1.0, - 0.25, - 2.0, - 0.05, - "×", - false, + RangeSpec { + default: 1.0, + min: 0.25, + max: 2.0, + step: 0.05, + suffix: "×", + integer: false, + }, ), range( "appearance.inactive_dim.duration_ms", "Focus transition", "Time taken to dim an inactive window.", - 150.0, - 0.0, - 600.0, - 10.0, - " ms", - false, + RangeSpec { + default: 150.0, + min: 0.0, + max: 600.0, + step: 10.0, + suffix: " ms", + integer: false, + }, ), ], Page::Keyboard => vec![ @@ -957,23 +1046,27 @@ pub fn fields(page: Page) -> Vec { "input.repeat_rate", "Key repeat", "Characters repeated each second while a key is held.", - 25.0, - 1.0, - 70.0, - 1.0, - " /s", - true, + RangeSpec { + default: 25.0, + min: 1.0, + max: 70.0, + step: 1.0, + suffix: " /s", + integer: true, + }, ), range( "input.repeat_delay_ms", "Delay before repeat", "How long to hold a key before it starts repeating.", - 600.0, - 150.0, - 1000.0, - 25.0, - " ms", - true, + RangeSpec { + default: 600.0, + min: 150.0, + max: 1000.0, + step: 25.0, + suffix: " ms", + integer: true, + }, ), toggle( "input.focus_follows_mouse", @@ -1003,12 +1096,14 @@ pub fn fields(page: Page) -> Vec { "input.touchpad.swipe_threshold", "Swipe distance", "Distance needed to navigate. Larger values reduce accidental swipes.", - 80.0, - 16.0, - 1000.0, - 8.0, - " px", - true, + RangeSpec { + default: 80.0, + min: 16.0, + max: 1000.0, + step: 8.0, + suffix: " px", + integer: true, + }, ), ], Page::Shortcuts => vec![toggle( diff --git a/shell/ferese-settings/src/theme_controls.rs b/shell/ferese-settings/src/theme_controls.rs index 590689b..91cab3c 100644 --- a/shell/ferese-settings/src/theme_controls.rs +++ b/shell/ferese-settings/src/theme_controls.rs @@ -199,12 +199,14 @@ impl App { let tile_id = gallery_id(&id, appearance); tiles = tiles.push(gallery::tile( family, - appearance, - active, - selected, - palette, - self.font, - tile_id, + gallery::TileOptions { + variant: appearance, + active, + selected, + palette, + font: self.font, + id: tile_id, + }, Message::Family(id, appearance), move |key| { gallery::neighbor(index, ids.len(), key) diff --git a/shell/ferese-settings/src/visuals.rs b/shell/ferese-settings/src/visuals.rs index f58d0c6..fd01efe 100644 --- a/shell/ferese-settings/src/visuals.rs +++ b/shell/ferese-settings/src/visuals.rs @@ -294,9 +294,11 @@ mod tests { fn presets_are_distinct_and_readable_on_settings_surfaces() { for (index, preset) in PRESETS.iter().enumerate() { let tokens = ferese_config::theme::preset(preset.id, preset.appearance).unwrap(); - let mut theme = ferese_config::theme::ResolvedTheme::default(); - theme.appearance = preset.appearance; - theme.tokens = tokens; + let theme = ferese_config::theme::ResolvedTheme { + appearance: preset.appearance, + tokens, + ..Default::default() + }; let palette = Palette::from_resolved(&theme); for background in [palette.background, palette.sidebar, palette.card] { assert!(contrast(palette.text, background) >= 4.5, "{} text", preset.name); @@ -313,10 +315,10 @@ mod tests { #[test] 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); 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" } else { "dark" @@ -343,9 +345,11 @@ mod tests { for (index, item) in PRESETS.iter().enumerate() { let _ = index; - let mut resolved = ferese_config::theme::ResolvedTheme::default(); - resolved.appearance = item.appearance; - resolved.tokens = ferese_config::theme::preset(item.id, item.appearance).unwrap(); + let resolved = ferese_config::theme::ResolvedTheme { + appearance: item.appearance, + tokens: ferese_config::theme::preset(item.id, item.appearance).unwrap(), + ..Default::default() + }; assert_eq!( Palette::from_resolved(&resolved).native_theme().cosmic().is_dark, item.appearance == ferese_config::theme::Appearance::Dark diff --git a/shell/ferese-settings/src/watch.rs b/shell/ferese-settings/src/watch.rs index c85575a..a745cab 100644 --- a/shell/ferese-settings/src/watch.rs +++ b/shell/ferese-settings/src/watch.rs @@ -1,5 +1,5 @@ //! Watch the parent directory so atomic editor saves keep working. -use std::path::PathBuf; +use std::path::Path; use std::time::Duration; use cosmic::iced::futures::{SinkExt, Stream}; @@ -8,8 +8,8 @@ use notify::{RecursiveMode, Watcher}; use crate::Message; use crate::store::Snapshot; -pub fn changes(path: &PathBuf) -> impl Stream + use<> { - let path = path.clone(); +pub fn changes(path: &Path) -> impl Stream + use<> { + let path = path.to_path_buf(); cosmic::iced::stream::channel(1, async move |mut output| { let (send, mut events) = tokio::sync::mpsc::channel(1); diff --git a/shell/ferese-shell/src/control.rs b/shell/ferese-shell/src/control.rs index b9b7f92..2867e26 100644 --- a/shell/ferese-shell/src/control.rs +++ b/shell/ferese-shell/src/control.rs @@ -37,7 +37,6 @@ pub(crate) struct WorkspaceSnapshot { #[derive(Clone, Debug)] pub(crate) struct WindowSnapshot { - pub(crate) id: u64, pub(crate) workspace: u64, pub(crate) app_id: String, pub(crate) title: String, @@ -138,12 +137,6 @@ impl ShellControl { 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) { self.shell .confirm_logout_with_inhibitors(serial, revision, token, u32::from(force)); @@ -268,8 +261,8 @@ impl Dispatch for ControlState { }); } ferese_shell_v1::Event::Window { - window_hi, - window_lo, + window_hi: _, + window_lo: _, workspace_hi, workspace_lo, app_id, @@ -285,7 +278,6 @@ impl Dispatch for ControlState { }; state.pending.windows.push(WindowSnapshot { - id: join_id(window_hi, window_lo), workspace: join_id(workspace_hi, workspace_lo), app_id, title, diff --git a/shell/ferese-shell/src/main.rs b/shell/ferese-shell/src/main.rs index d69e3a9..e3f304a 100644 --- a/shell/ferese-shell/src/main.rs +++ b/shell/ferese-shell/src/main.rs @@ -221,7 +221,7 @@ struct OutputSurfaces { #[derive(Clone, Debug)] enum Message { - ThemeChanged(ferese_config::theme::Snapshot), + ThemeChanged(Box), ThemeMode(ferese_config::theme::Mode), ThemeModeSet(Result<(), String>), BeginNoteEdit(String), @@ -340,7 +340,7 @@ impl cosmic::Application for FereseShell { fn subscription(&self) -> Subscription { Subscription::batch([ - ferese_theme::service::subscription().map(Message::ThemeChanged), + ferese_theme::service::subscription().map(|snapshot| Message::ThemeChanged(Box::new(snapshot))), if self .system_modal .as_ref() @@ -1208,7 +1208,6 @@ mod tests { }], workspaces: Vec::new(), windows: vec![WindowSnapshot { - id: 1, workspace, app_id: "dev.ferese.Test".to_owned(), title: "Test".to_owned(), diff --git a/shell/ferese-shell/src/status_ui/bluetooth.rs b/shell/ferese-shell/src/status_ui/bluetooth.rs index aeb0a68..befd444 100644 --- a/shell/ferese-shell/src/status_ui/bluetooth.rs +++ b/shell/ferese-shell/src/status_ui/bluetooth.rs @@ -6,7 +6,7 @@ use super::bar::status_icon; use super::controls::status_summary; use super::{Menu, MenuRows, MenuStyle}; use crate::status::Action; -use crate::{FereseShell, Message, color_with_opacity, text}; +use crate::{FereseShell, Message, text}; const DEVICE_LIST_HEIGHT: f32 = 180.0; @@ -15,12 +15,7 @@ fn device_list_height(count: usize) -> Option { } pub(super) fn view<'a>(shell: &'a FereseShell, mut rows: MenuRows<'a>, style: MenuStyle) -> MenuRows<'a> { - let MenuStyle { - theme, - primary, - muted, - opacity: p, - } = style; + let MenuStyle { muted, .. } = style; if let Some(b) = &shell.status.bluetooth { rows = rows.push(status_summary( 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 { "Your connected devices" }, - primary, - muted, - color_with_opacity(theme.accent, p), - p, + style, b.enabled, Some((b.enabled, Action::Bluetooth(!b.enabled))), )); diff --git a/shell/ferese-shell/src/status_ui/controls.rs b/shell/ferese-shell/src/status_ui/controls.rs index 1bfefe7..97d1370 100644 --- a/shell/ferese-shell/src/status_ui/controls.rs +++ b/shell/ferese-shell/src/status_ui/controls.rs @@ -3,19 +3,23 @@ use cosmic::widget::{button, column, container, row, slider}; use cosmic::{Element, theme}; 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>( source: &'static [u8], title: &'a str, subtitle: &'a str, - primary: Color, - muted: Color, - accent: Color, - opacity: f32, + style: super::MenuStyle, enabled: bool, toggle: Option<(bool, Action)>, ) -> Element<'a, cosmic::Action> { + 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 = container(accented_icon(source, 22, primary, accent)) .width(32) diff --git a/shell/ferese-shell/src/status_ui/network.rs b/shell/ferese-shell/src/status_ui/network.rs index 0107170..3bdff81 100644 --- a/shell/ferese-shell/src/status_ui/network.rs +++ b/shell/ferese-shell/src/status_ui/network.rs @@ -5,14 +5,11 @@ use super::bar::status_icon; use super::controls::{level_meter, shell_switch, status_summary}; use super::{Menu, MenuRows, MenuStyle}; 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> { let MenuStyle { - theme, - primary, - muted, - opacity: p, + primary, opacity: p, .. } = style; if let Some(n) = &shell.status.network { 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 { "Wireless connections are paused" }, - primary, - muted, - color_with_opacity(theme.accent, p), - p, + style, n.enabled, None, )); diff --git a/shell/ferese-shell/src/status_ui/system.rs b/shell/ferese-shell/src/status_ui/system.rs index 94b53b7..c94d55f 100644 --- a/shell/ferese-shell/src/status_ui/system.rs +++ b/shell/ferese-shell/src/status_ui/system.rs @@ -178,21 +178,6 @@ fn connection_control<'a>( 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>( shell: &'a FereseShell, mut heading: super::MenuHeading<'a>, @@ -229,3 +214,18 @@ pub(super) fn heading<'a>( } 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('…')); + } +}