make button focus ring conditional on tab press

This commit is contained in:
Samuel Onoja committed 2026-10-03 04:29:06 +01:00
1 parent 64a80dc069
commit 1d7a472847
5 files changed
+116 -9

No files matched your search

+67 -5
View File
@@ -9,11 +9,24 @@ pub fn surface(background: Color, radius: f32) -> theme::Container<'static> {
} }
pub fn button_style(p: Palette, selected: bool) -> theme::Button { pub fn button_style(p: Palette, selected: bool) -> theme::Button {
styled_button(p, selected, false, 1.0) button_style_with_focus(p, selected, true)
}
pub fn button_style_with_focus(p: Palette, selected: bool, focus_visible: bool) -> theme::Button {
styled_button(p, selected, false, 1.0, focus_visible)
} }
/// Selection cards share native button states while revealing the modal material. /// Selection cards share native button states while revealing the modal material.
pub fn material_button_style(p: Palette, selected: bool, material_opacity: f32) -> theme::Button { pub fn material_button_style(p: Palette, selected: bool, material_opacity: f32) -> theme::Button {
material_button_style_with_focus(p, selected, material_opacity, true)
}
pub fn material_button_style_with_focus(
p: Palette,
selected: bool,
material_opacity: f32,
focus_visible: bool,
) -> theme::Button {
let opacity = if material_opacity.is_finite() { let opacity = if material_opacity.is_finite() {
material_opacity.clamp(0.0, 1.0) material_opacity.clamp(0.0, 1.0)
} else { } else {
@@ -26,7 +39,7 @@ pub fn material_button_style(p: Palette, selected: bool, material_opacity: f32)
1.0 1.0
}; };
styled_button(p, selected, false, fill) styled_button(p, selected, false, fill, focus_visible)
} }
pub fn text_button<'a, M: Clone + 'a>( pub fn text_button<'a, M: Clone + 'a>(
@@ -82,7 +95,7 @@ pub(crate) fn switch_colors(palette: Palette, enabled: bool, hovered: bool) -> (
} }
pub fn navigation_style(p: Palette, selected: bool) -> theme::Button { pub fn navigation_style(p: Palette, selected: bool) -> theme::Button {
styled_button(p, selected, true, 1.0) styled_button(p, selected, true, 1.0, true)
} }
pub fn settings_input(p: Palette) -> theme::TextInput { pub fn settings_input(p: Palette) -> theme::TextInput {
@@ -128,7 +141,7 @@ pub fn select<'a, M: 'a>(
})) }))
} }
fn styled_button(p: Palette, selected: bool, navigation: bool, opacity: f32) -> theme::Button { fn styled_button(p: Palette, selected: bool, navigation: bool, opacity: f32, focus_visible: bool) -> theme::Button {
let style = move |hover: bool, focused: bool| { let style = move |hover: bool, focused: bool| {
let background = if selected { let background = if selected {
mix(p.sidebar, p.accent, if hover { 0.24 } else { 0.17 }) mix(p.sidebar, p.accent, if hover { 0.24 } else { 0.17 })
@@ -169,7 +182,7 @@ fn styled_button(p: Palette, selected: bool, navigation: bool, opacity: f32) ->
} else { } else {
Color::TRANSPARENT Color::TRANSPARENT
}, },
outline_width: if focused { 2. } else { 0. }, outline_width: if focused && focus_visible { 2. } else { 0. },
outline_color: p.accent, outline_color: p.accent,
overlay: None, overlay: None,
shadow_offset: Vector::ZERO, shadow_offset: Vector::ZERO,
@@ -350,6 +363,55 @@ pub fn filled_button(fill: Color, foreground: Color, radius: f32, opacity: f32)
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn hiding_modal_focus_preserves_button_borders_and_materials() {
use cosmic::widget::button::Catalog;
let palette = Palette::from_resolved(&ferese_config::theme::default_theme());
let theme = palette.native_theme();
for selected in [false, true] {
for opacity in [0.65, 1.0] {
let hidden = material_button_style_with_focus(palette, selected, opacity, false);
let visible = material_button_style_with_focus(palette, selected, opacity, true);
for (hidden, visible, resting) in [
(
theme.active(true, selected, &hidden),
theme.active(true, selected, &visible),
theme.active(false, selected, &visible),
),
(
theme.hovered(true, selected, &hidden),
theme.hovered(true, selected, &visible),
theme.hovered(false, selected, &visible),
),
(
theme.pressed(true, selected, &hidden),
theme.pressed(true, selected, &visible),
theme.pressed(false, selected, &visible),
),
] {
assert_eq!(hidden.outline_width, 0.0);
assert_eq!(visible.outline_width, 2.0);
assert_eq!(resting.outline_width, 0.0);
assert_eq!(hidden.border_width, 1.0);
assert_eq!(hidden.border_width, visible.border_width);
assert_eq!(hidden.border_color, visible.border_color);
assert_eq!(hidden.border_radius, visible.border_radius);
assert_eq!(hidden.background, visible.background);
assert_eq!(hidden.text_color, visible.text_color);
}
}
let hidden = button_style_with_focus(palette, selected, false);
let visible = button_style_with_focus(palette, selected, true);
assert_eq!(theme.active(true, selected, &hidden).outline_width, 0.0);
assert_eq!(theme.active(true, selected, &visible).outline_width, 2.0);
assert_eq!(theme.active(true, selected, &hidden).border_width, 1.0);
}
}
#[test] #[test]
fn authentication_button_does_not_reverse_enabled_and_disabled_text() { fn authentication_button_does_not_reverse_enabled_and_disabled_text() {
use cosmic::widget::button::Catalog; use cosmic::widget::button::Catalog;
+13 -1
View File
@@ -391,6 +391,7 @@ impl FereseShell {
f32::from(self.config.theme.surface_popover[3]) / 255.0 f32::from(self.config.theme.surface_popover[3]) / 255.0
}; };
let model = &self.display_mode; let model = &self.display_mode;
let focus_visible = self.system_modal.as_ref().is_some_and(|modal| modal.focus_visible);
let pending = model.pending(); let pending = model.pending();
let mut header = row![ let mut header = row![
text("Display mode").size(17), text("Display mode").size(17),
@@ -435,10 +436,11 @@ impl FereseShell {
.id(format!("ferese-display-mode-{}", mode.name()).into()) .id(format!("ferese-display-mode-{}", mode.name()).into())
.width(Length::Fill) .width(Length::Fill)
.padding([14, 6]) .padding([14, 6])
.class(ferese_theme::controls::material_button_style( .class(ferese_theme::controls::material_button_style_with_focus(
palette, palette,
model.selected == mode, model.selected == mode,
material_opacity, material_opacity,
focus_visible,
)) ))
.on_press_maybe(available.then_some(cosmic::Action::App(Message::SelectDisplayMode(mode)))), .on_press_maybe(available.then_some(cosmic::Action::App(Message::SelectDisplayMode(mode)))),
); );
@@ -504,6 +506,11 @@ impl FereseShell {
palette, palette,
false false
) )
.class(ferese_theme::controls::button_style_with_focus(
palette,
false,
focus_visible
))
.id("ferese-modal-cancel".into()) .id("ferese-modal-cancel".into())
.on_press_maybe((!model.busy).then_some(cosmic::Action::App(cancel))), .on_press_maybe((!model.busy).then_some(cosmic::Action::App(cancel))),
ferese_theme::controls::text_button( ferese_theme::controls::text_button(
@@ -512,6 +519,11 @@ impl FereseShell {
palette, palette,
true true
) )
.class(ferese_theme::controls::button_style_with_focus(
palette,
true,
focus_visible
))
.id("ferese-display-apply".into()) .id("ferese-display-apply".into())
.on_press_maybe(can_apply.then_some(cosmic::Action::App(apply))), .on_press_maybe(can_apply.then_some(cosmic::Action::App(apply))),
] ]
+1
View File
@@ -368,6 +368,7 @@ impl cosmic::Application for FereseShell {
.load(Ordering::Relaxed) .load(Ordering::Relaxed)
.then_some(Message::Event(event, id)), .then_some(Message::Event(event, id)),
Event::Keyboard(_) Event::Keyboard(_)
| Event::Mouse(cosmic::iced::mouse::Event::ButtonPressed(_))
| Event::Window(window::Event::Opened { .. } | window::Event::Closed) | Event::Window(window::Event::Opened { .. } | window::Event::Closed)
| Event::PlatformSpecific(PlatformSpecific::Wayland( | Event::PlatformSpecific(PlatformSpecific::Wayland(
wayland::Event::Popup(..) | wayland::Event::Layer(..) | wayland::Event::Output(..), wayland::Event::Popup(..) | wayland::Event::Layer(..) | wayland::Event::Output(..),
+7
View File
@@ -164,6 +164,13 @@ impl FereseShell {
} }
pub(super) fn handle_event(&mut self, event: Event, id: window::Id) -> Task<Message> { pub(super) fn handle_event(&mut self, event: Event, id: window::Id) -> Task<Message> {
if let Event::Mouse(cosmic::iced::mouse::Event::ButtonPressed(_)) = &event
&& let Some(modal) = &mut self.system_modal
&& modal.contains(id)
{
modal.focus_visible = false;
}
if let Event::Mouse(mouse) = &event { if let Event::Mouse(mouse) = &event {
if !self if !self
.outputs .outputs
+28 -3
View File
@@ -69,6 +69,7 @@ pub(super) struct SystemModal {
error: Option<String>, error: Option<String>,
inhibitors: Option<compositor_ipc::Approval>, inhibitors: Option<compositor_ipc::Approval>,
keyboard_nav: bool, keyboard_nav: bool,
pub(super) focus_visible: bool,
} }
impl SystemModal { impl SystemModal {
@@ -236,6 +237,7 @@ impl FereseShell {
error: None, error: None,
inhibitors: mode.is_none().then(compositor_ipc::Approval::default), inhibitors: mode.is_none().then(compositor_ipc::Approval::default),
keyboard_nav, keyboard_nav,
focus_visible: false,
}); });
Task::batch(tasks) Task::batch(tasks)
} }
@@ -387,10 +389,17 @@ impl FereseShell {
} }
} }
pub(super) fn navigate_system_modal(&self, backwards: bool) -> Task<Message> { pub(super) fn navigate_system_modal(&mut self, backwards: bool) -> Task<Message> {
if self.system_modal.as_ref().is_none_or(|modal| modal.motion.closing()) { let Some(modal) = &mut self.system_modal else {
return Task::none();
};
if modal.motion.closing() {
return Task::none(); return Task::none();
} }
modal.focus_visible = true;
cosmic::iced::advanced::widget::operate(modal_focus_operation(backwards)) cosmic::iced::advanced::widget::operate(modal_focus_operation(backwards))
} }
@@ -536,9 +545,20 @@ impl FereseShell {
row![ row![
Space::new().width(Length::Fill), Space::new().width(Length::Fill),
ferese_theme::controls::text_button("Cancel", shell_font(), palette, false) ferese_theme::controls::text_button("Cancel", shell_font(), palette, false)
.class(ferese_theme::controls::button_style_with_focus(
palette,
false,
modal.focus_visible,
))
.id("ferese-modal-cancel".into()) .id("ferese-modal-cancel".into())
.on_press(cosmic::Action::App(Message::CancelPower)), .on_press(cosmic::Action::App(Message::CancelPower)),
ferese_theme::controls::text_button(label, shell_font(), palette, true).on_press_maybe( ferese_theme::controls::text_button(label, shell_font(), palette, true)
.class(ferese_theme::controls::button_style_with_focus(
palette,
true,
modal.focus_visible,
))
.on_press_maybe(
modal modal
.inhibitors .inhibitors
.is_some() .is_some()
@@ -582,6 +602,11 @@ impl FereseShell {
container(row![ container(row![
Space::new().width(Length::Fill), Space::new().width(Length::Fill),
ferese_theme::controls::text_button("Got it", shell_font(), palette, true,) ferese_theme::controls::text_button("Got it", shell_font(), palette, true,)
.class(ferese_theme::controls::button_style_with_focus(
palette,
true,
modal.focus_visible,
))
.id("ferese-modal-cancel".into()) .id("ferese-modal-cancel".into())
.on_press(cosmic::Action::App(Message::CancelPower)), .on_press(cosmic::Action::App(Message::CancelPower)),
]) ])