From 557b0e4b7bc3a33df819f7359b426a3d7de7ae88 Mon Sep 17 00:00:00 2001
From: LynithDev <61880709+LynithDev@users.noreply.github.com>
Date: Mon, 13 Jul 2026 16:10:21 +0200
Subject: [PATCH] feat(ui): Make the launcher navigatable with the keyboard
---
.../oneclient_app/src/components/dropdown.rs | 21 +-
.../src/components/major_version_card.rs | 21 +-
.../src/components/navbar/app_navbar.rs | 4 +-
.../src/components/pagination.rs | 76 ++---
.../src/components/recents_row.rs | 33 +-
.../src/components/scrollview.rs | 47 +--
.../src/components/segmented_control.rs | 150 +++++----
.../oneclient_app/src/components/tab_bar.rs | 113 ++++---
.../oneclient_app/src/components/toggle.rs | 16 +-
.../src/layout/settings_shell.rs | 24 +-
.../src/view/app/browser/index/cards.rs | 65 +++-
.../src/view/app/browser/index/sidebar.rs | 12 +-
.../src/view/app/browser/package/mod.rs | 2 +-
.../src/view/app/browser/package/panels.rs | 41 +--
.../src/view/app/browser/package/sidebar.rs | 178 +++++++----
.../src/view/app/cluster/cluster_settings.rs | 15 +-
.../src/view/app/cluster/logs.rs | 27 +-
.../view/app/cluster/package_manager/views.rs | 289 +++++++++++-------
18 files changed, 735 insertions(+), 399 deletions(-)
diff --git a/packages/oneclient_app/src/components/dropdown.rs b/packages/oneclient_app/src/components/dropdown.rs
index a8cd3e7b..f40a0ebf 100644
--- a/packages/oneclient_app/src/components/dropdown.rs
+++ b/packages/oneclient_app/src/components/dropdown.rs
@@ -59,6 +59,9 @@ impl Component for Dropdown {
let mut open = use_state(|| false);
let mut hovering = use_state(|| false);
+ let a11y_id = use_a11y();
+ let focus = use_focus(a11y_id);
+
let mut button_area = use_state(|| None::);
let mut list_size = use_state(|| None::);
@@ -115,13 +118,19 @@ impl Component for Dropdown {
.padding(Gaps::new_symmetric(0., 8.))
.corner_radius(CornerRadius::new_all(6.))
.background(trigger_bg)
+ .a11y_id(a11y_id)
+ .a11y_focusable(true)
+ .a11y_role(AccessibilityRole::Button)
+ .maybe(focus().is_focused(), |el| {
+ el.border(border_all_color(1., colors::brand()))
+ })
.on_pointer_enter(move |_| hovering.set(true))
.on_pointer_leave(move |_| hovering.set(false))
.on_global_pointer_press(on_global_pointer_press)
.on_sized(move |e: Event| {
button_area.set_if_modified(Some(e.area));
})
- .on_press(move |e: Event| {
+ .on_all_press(move |e: Event| {
e.prevent_default();
e.stop_propagation();
open.toggle();
@@ -211,18 +220,24 @@ impl Component for DropdownOption {
fn render(&self) -> impl IntoElement {
let mut hovering = use_state(|| false);
+ let a11y_id = use_a11y();
+ let focus = use_focus(a11y_id);
+
rect()
.width(Size::fill())
.padding(Gaps::new_symmetric(6., 8.))
.corner_radius(CornerRadius::new_all(6.))
- .background(if hovering() {
+ .a11y_id(a11y_id)
+ .a11y_focusable(true)
+ .a11y_role(AccessibilityRole::Button)
+ .background(if hovering() || focus().is_focused() {
colors::ghost_overlay_hover()
} else {
Color::TRANSPARENT
})
.on_pointer_enter(move |_| hovering.set(true))
.on_pointer_leave(move |_| hovering.set(false))
- .on_press(self.on_press.clone())
+ .on_all_press(self.on_press.clone())
.child(
label()
.text(self.text.clone())
diff --git a/packages/oneclient_app/src/components/major_version_card.rs b/packages/oneclient_app/src/components/major_version_card.rs
index bec57c63..b8c1600c 100644
--- a/packages/oneclient_app/src/components/major_version_card.rs
+++ b/packages/oneclient_app/src/components/major_version_card.rs
@@ -46,19 +46,25 @@ impl Component for MajorVersionCard {
fn render(&self) -> impl IntoElement {
let mut hovering = use_state(|| false);
+ let a11y_id = use_a11y();
+ let focus = use_focus(a11y_id);
+
let selected = self.selected;
let hovered = *hovering.read();
+ let focused = focus().is_focused();
let on_press = self.on_press.clone();
- let opacity = if selected {
- 1.0
- } else if hovered {
- 0.85
+ let opacity = if selected || hovered || focused {
+ if selected {
+ 1.0
+ } else {
+ 0.85
+ }
} else {
0.6
};
- let border_color = if selected {
+ let border_color = if selected || focused {
colors::brand()
} else if hovered {
colors::component_border_hover()
@@ -70,7 +76,10 @@ impl Component for MajorVersionCard {
.key(self.major)
.width(Size::flex(1.0))
.height(Size::px(CARD_HEIGHT_PX))
- .on_press(move |e| on_press.call(e))
+ .a11y_id(a11y_id)
+ .a11y_focusable(true)
+ .a11y_role(AccessibilityRole::Button)
+ .on_all_press(move |e| on_press.call(e))
.on_pointer_enter(move |_| {
*hovering.write() = true;
})
diff --git a/packages/oneclient_app/src/components/navbar/app_navbar.rs b/packages/oneclient_app/src/components/navbar/app_navbar.rs
index 9f4caf5e..898dfb7c 100644
--- a/packages/oneclient_app/src/components/navbar/app_navbar.rs
+++ b/packages/oneclient_app/src/components/navbar/app_navbar.rs
@@ -106,7 +106,7 @@ impl Component for NavLink {
Cursor::set(CursorIcon::default());
});
- let color = if active || *hovering.peek() || focused.peek().is_focused() {
+ let color = if active || hovering() || focused().is_focused() {
theme::colors::fg_primary()
} else {
theme::colors::fg_secondary()
@@ -114,7 +114,7 @@ impl Component for NavLink {
let underline_width = if active {
27.
- } else if *hovering.peek() || focused.peek().is_focused() {
+ } else if hovering() || focused().is_focused() {
18.
} else {
0.
diff --git a/packages/oneclient_app/src/components/pagination.rs b/packages/oneclient_app/src/components/pagination.rs
index 9a3c072b..bb3913b4 100644
--- a/packages/oneclient_app/src/components/pagination.rs
+++ b/packages/oneclient_app/src/components/pagination.rs
@@ -1,6 +1,6 @@
use freya::prelude::*;
-use crate::components::{Icon, IconType};
+use crate::components::{Button, Icon, IconType};
use crate::theme::colors;
use crate::ui::border_all_color;
@@ -45,18 +45,11 @@ impl Component for Pagination {
let icon_btn = move |target: usize, enabled: bool, icon: IconType| {
let mut page = page;
- rect()
- .center()
- .width(Size::px(32.))
- .height(Size::px(32.))
- .corner_radius(CornerRadius::new_all(8.))
- .background(colors::component_bg())
- .border(border_all_color(1., colors::component_border()))
- .maybe(enabled, |el| {
- el.on_pointer_enter(|_| Cursor::set(CursorIcon::Pointer))
- .on_pointer_leave(|_| Cursor::set(CursorIcon::default()))
- .on_press(move |_| page.set(target))
- })
+ Button::new()
+ .secondary()
+ .icon()
+ .enabled(enabled)
+ .on_press(move |_| page.set(target))
.child(Icon::new(icon).size(14.).color(if enabled {
colors::fg_primary()
} else {
@@ -68,43 +61,38 @@ impl Component for Pagination {
let num_btn = move |target: usize| {
let mut page = page;
let is_current = target == current;
- rect()
- .center()
+
+ if is_current {
+ return rect()
+ .center()
+ .width(Size::px(32.))
+ .height(Size::px(32.))
+ .corner_radius(CornerRadius::new_all(8.))
+ .background(colors::component_bg_pressed())
+ .border(border_all_color(1., colors::brand()))
+ .child(
+ label()
+ .text(format!("{}", target + 1))
+ .font_size(12.)
+ .font_weight(FontWeight::SEMI_BOLD)
+ .color(colors::fg_primary()),
+ )
+ .into_element();
+ }
+
+ Button::new()
+ .secondary()
.width(Size::px(32.))
.height(Size::px(32.))
- .corner_radius(CornerRadius::new_all(8.))
- .background(if is_current {
- colors::component_bg_pressed()
- } else {
- colors::component_bg()
- })
- .border(border_all_color(
- 1.,
- if is_current {
- colors::brand()
- } else {
- colors::component_border()
- },
- ))
- .maybe(!is_current, |el| {
- el.on_pointer_enter(|_| Cursor::set(CursorIcon::Pointer))
- .on_pointer_leave(|_| Cursor::set(CursorIcon::default()))
- .on_press(move |_| page.set(target))
- })
+ .padding(Gaps::new_symmetric(6., 0.))
+ .on_press(move |_| page.set(target))
.child(
label()
.text(format!("{}", target + 1))
.font_size(12.)
- .font_weight(if is_current {
- FontWeight::SEMI_BOLD
- } else {
- FontWeight::NORMAL
- })
- .color(if is_current {
- colors::fg_primary()
- } else {
- colors::fg_secondary()
- }),
+ .font_weight(FontWeight::NORMAL)
+ .max_lines(1)
+ .color(colors::fg_secondary()),
)
.into_element()
};
diff --git a/packages/oneclient_app/src/components/recents_row.rs b/packages/oneclient_app/src/components/recents_row.rs
index e586793a..3a3c80bf 100644
--- a/packages/oneclient_app/src/components/recents_row.rs
+++ b/packages/oneclient_app/src/components/recents_row.rs
@@ -101,6 +101,10 @@ impl Component for ClusterCard {
let active = *active_id.read() == Some(self.cluster.id);
let mut hovering = use_state(|| false);
+ let a11y_id = use_a11y();
+ let focus = use_focus(a11y_id);
+ let focused = focus().is_focused();
+
let total = CARD_MS as f32 + (self.count.saturating_sub(1) as f32) * STAGGER_MS as f32;
let elapsed = self.progress * total;
let local =
@@ -135,7 +139,7 @@ impl Component for ClusterCard {
.corner_radius(CornerRadius::new_all(12.))
.overflow(Overflow::Clip)
.border(
- if active {
+ if active || focused {
border_all_color(2., colors::brand())
} else if *hovering.read() {
border_all_color(1., colors::component_border_hover())
@@ -144,6 +148,9 @@ impl Component for ClusterCard {
}
.alignment(BorderAlignment::Outer),
)
+ .a11y_id(a11y_id)
+ .a11y_focusable(true)
+ .a11y_role(AccessibilityRole::Button)
.on_pointer_over(move |_| {
if !*hovering.peek() {
*hovering.write() = true;
@@ -160,7 +167,7 @@ impl Component for ClusterCard {
.y(0.)
.color(Color::from_af32rgb(0.3, 0, 0, 0)),
)
- .on_press(on_press)
+ .on_all_press(on_press)
.child(
rect()
.width(Size::fill())
@@ -220,17 +227,31 @@ impl Component for OtherVersionsTile {
fn render(&self) -> impl IntoElement {
let mut hovering = use_state(|| false);
+ let a11y_id = use_a11y();
+ let focus = use_focus(a11y_id);
+ let focused = focus().is_focused();
+
rect()
.width(Size::px(MORE_TILE_WIDTH_PX))
.height(Size::fill())
.corner_radius(CornerRadius::new_all(12.))
- .border(border_all(1.))
+ .border(
+ if focused {
+ border_all_color(2., colors::brand())
+ } else {
+ border_all(1.)
+ }
+ .alignment(BorderAlignment::Outer),
+ )
.background(if *hovering.read() {
colors::ghost_overlay_pressed()
} else {
colors::ghost_overlay_hover()
})
.blur(32.)
+ .a11y_id(a11y_id)
+ .a11y_focusable(true)
+ .a11y_role(AccessibilityRole::Button)
.on_pointer_enter(move |_| {
*hovering.write() = true;
})
@@ -245,7 +266,7 @@ impl Component for OtherVersionsTile {
.y(0.)
.color(Color::from_af32rgb(0.3, 0, 0, 0)),
)
- .on_press(|_| {
+ .on_all_press(|_| {
let _ = RouterContext::get().push(Route::Clusters {});
})
.child(
@@ -259,10 +280,6 @@ impl Component for OtherVersionsTile {
}
fn recent_card_slots_for_width(row_width_px: f32) -> usize {
- // Layout is: [card gap]* more-tile — i.e. N cards each followed by a gap,
- // then the fixed-width more-tile. So width for N cards is:
- // N * MIN_CARD_WIDTH + N * GAP + MORE_TILE_WIDTH
- // Solve for the largest N that fits.
if row_width_px <= MORE_TILE_WIDTH_PX + CARD_GAP_PX {
return 0;
}
diff --git a/packages/oneclient_app/src/components/scrollview.rs b/packages/oneclient_app/src/components/scrollview.rs
index b33b207a..ba49b6c3 100644
--- a/packages/oneclient_app/src/components/scrollview.rs
+++ b/packages/oneclient_app/src/components/scrollview.rs
@@ -311,36 +311,42 @@ impl ScrollArea {
let can_scroll_h = horizontal && vp_w > 0. && vp_w < content_w;
let on_wheel = move |e: Event| {
- let wants_h = horizontal && *shift_held.read();
+ let shift = *shift_held.read();
+
+ let h_delta = if horizontal {
+ if shift && e.delta_x == 0.0 { e.delta_y } else { e.delta_x }
+ } else {
+ 0.0
+ };
+ let v_delta = if horizontal && shift { 0.0 } else { e.delta_y };
+
+ let do_h = h_delta != 0.0 && can_scroll_h;
+ let do_v = v_delta != 0.0 && can_scroll_v;
// check if scrollable
- if (wants_h && !can_scroll_h) || !can_scroll_v {
+ if !do_h && !do_v {
return;
}
e.stop_propagation();
- if (e.delta_y != 0.0 || e.delta_x != 0.0)
- && let Some(cb) = &us_wheel
- {
+ if let Some(cb) = &us_wheel {
cb.call(());
}
- if wants_h {
- let delta = if e.delta_x != 0.0 { e.delta_x } else { e.delta_y };
- if delta != 0.0 {
- let cur_x = corrected_scroll(content_w, vp_w, *scroll_x.read());
- let new_x = scroll_pos_from_wheel(delta as f32, content_w, vp_w, cur_x);
- scroll_x.set(new_x as f32);
- }
- return;
+ if do_h {
+ let cur_x = corrected_scroll(content_w, vp_w, *scroll_x.read());
+ let new_x = scroll_pos_from_wheel(h_delta as f32, content_w, vp_w, cur_x);
+ scroll_x.set(new_x as f32);
}
- let (_, cur_y) = controller.into();
- let current = corrected_scroll(ct_h, vp_h, cur_y as f32);
- let new_y = scroll_pos_from_wheel(e.delta_y as f32, ct_h, vp_h, current);
- if new_y != cur_y {
- controller.scroll_to_y(new_y);
+ if do_v {
+ let (_, cur_y) = controller.into();
+ let current = corrected_scroll(ct_h, vp_h, cur_y as f32);
+ let new_y = scroll_pos_from_wheel(v_delta as f32, ct_h, vp_h, current);
+ if new_y != cur_y {
+ controller.scroll_to_y(new_y);
+ }
}
};
@@ -403,6 +409,11 @@ impl ScrollArea {
rect()
.width(self.width.clone())
.height(self.height.clone())
+ .scrollable(true)
+ .a11y_builder(move |node| {
+ node.set_scroll_x(corrected_x as f64);
+ node.set_scroll_y(corrected_y as f64);
+ })
.on_wheel(on_wheel)
.on_capture_global_pointer_move(on_global_move)
.on_capture_global_pointer_press(on_global_release)
diff --git a/packages/oneclient_app/src/components/segmented_control.rs b/packages/oneclient_app/src/components/segmented_control.rs
index 87b91dba..384711a7 100644
--- a/packages/oneclient_app/src/components/segmented_control.rs
+++ b/packages/oneclient_app/src/components/segmented_control.rs
@@ -101,16 +101,6 @@ impl Component for SegmentedControl {
let equal_width = self.equal_width;
let disabled = self.disabled;
- let active_color = |is_selected: bool| {
- if disabled {
- colors::fg_secondary().with_a(110)
- } else if is_selected {
- colors::fg_primary()
- } else {
- colors::fg_secondary()
- }
- };
-
rect()
.horizontal()
.height(Size::px(self.height))
@@ -121,55 +111,103 @@ impl Component for SegmentedControl {
.background(colors::component_bg())
.border(border_all_color(1., colors::component_border()))
.children(self.segments.iter().map(move |seg| {
- let value = seg.value;
- let is_selected = value == active;
- let mut selected = selected;
- let label_text = seg.label.clone();
+ SegmentButton {
+ selected,
+ value: seg.value,
+ is_selected: seg.value == active,
+ icon: seg.icon,
+ label: seg.label.clone(),
+ icon_size,
+ tint,
+ equal_width,
+ disabled,
+ }
+ .into_element()
+ }))
+ }
+}
- rect()
- .horizontal()
- .height(Size::fill())
- .cross_align(Alignment::Center)
- .spacing(6.)
- .corner_radius(CornerRadius::new_all(6.))
- .map(equal_width, |el, w| {
- el.width(Size::px(w)).main_align(Alignment::Center)
- })
- .maybe(equal_width.is_none(), |el| {
- el.padding(Gaps::new_symmetric(0., 12.))
- })
- .background(if is_selected {
- colors::component_bg_pressed()
+#[derive(PartialEq)]
+struct SegmentButton {
+ selected: State,
+ value: T,
+ is_selected: bool,
+ icon: Option,
+ label: Option,
+ icon_size: Option,
+ tint: bool,
+ equal_width: Option,
+ disabled: bool,
+}
+
+impl Component for SegmentButton {
+ fn render(&self) -> impl IntoElement {
+ let a11y_id = use_a11y();
+ let focus = use_focus(a11y_id);
+
+ let mut selected = self.selected;
+ let value = self.value;
+ let is_selected = self.is_selected;
+ let disabled = self.disabled;
+ let equal_width = self.equal_width;
+ let focused = focus().is_focused();
+
+ let content_color = if disabled {
+ colors::fg_secondary().with_a(110)
+ } else if is_selected {
+ colors::fg_primary()
+ } else {
+ colors::fg_secondary()
+ };
+
+ rect()
+ .horizontal()
+ .height(Size::fill())
+ .cross_align(Alignment::Center)
+ .spacing(6.)
+ .corner_radius(CornerRadius::new_all(6.))
+ .a11y_id(a11y_id)
+ .a11y_focusable(!disabled)
+ .a11y_role(AccessibilityRole::Button)
+ .map(equal_width, |el, w| {
+ el.width(Size::px(w)).main_align(Alignment::Center)
+ })
+ .maybe(equal_width.is_none(), |el| {
+ el.padding(Gaps::new_symmetric(0., 12.))
+ })
+ .background(if is_selected {
+ colors::component_bg_pressed()
+ } else {
+ Color::TRANSPARENT
+ })
+ .maybe(focused && !disabled, |el| {
+ el.border(border_all_color(1., colors::brand()))
+ })
+ .maybe(!disabled, |el| {
+ el.on_pointer_enter(|_| Cursor::set(CursorIcon::Pointer))
+ .on_pointer_leave(|_| Cursor::set(CursorIcon::default()))
+ .on_all_press(move |_| *selected.write() = value)
+ })
+ .maybe_child(self.icon.map(|icon| {
+ let mut ic = Icon::new(icon);
+ if let Some(size) = self.icon_size {
+ ic = ic.size(size);
+ }
+ if self.tint {
+ ic = ic.color(content_color);
+ }
+ ic.into_element()
+ }))
+ .maybe_child(self.label.clone().map(|text| {
+ label()
+ .text(text)
+ .font_size(13.)
+ .font_weight(if is_selected {
+ FontWeight::MEDIUM
} else {
- Color::TRANSPARENT
+ FontWeight::NORMAL
})
- .maybe(!disabled, |el| {
- el.on_pointer_enter(|_| Cursor::set(CursorIcon::Pointer))
- .on_pointer_leave(|_| Cursor::set(CursorIcon::default()))
- .on_press(move |_| *selected.write() = value)
- })
- .maybe_child(seg.icon.map(|icon| {
- let mut ic = Icon::new(icon);
- if let Some(size) = icon_size {
- ic = ic.size(size);
- }
- if tint {
- ic = ic.color(active_color(is_selected));
- }
- ic.into_element()
- }))
- .maybe_child(label_text.map(|text| {
- label()
- .text(text)
- .font_size(13.)
- .font_weight(if is_selected {
- FontWeight::MEDIUM
- } else {
- FontWeight::NORMAL
- })
- .color(active_color(is_selected))
- .into_element()
- }))
+ .color(content_color)
.into_element()
}))
}
diff --git a/packages/oneclient_app/src/components/tab_bar.rs b/packages/oneclient_app/src/components/tab_bar.rs
index 910f98c4..f79d3409 100644
--- a/packages/oneclient_app/src/components/tab_bar.rs
+++ b/packages/oneclient_app/src/components/tab_bar.rs
@@ -6,7 +6,7 @@ pub struct TabItem {
label: String,
active: bool,
count: Option,
- on_press: Option)>>,
+ on_press: Option>>,
}
impl TabItem {
@@ -25,8 +25,8 @@ impl TabItem {
self
}
- pub fn on_press(mut self, handler: impl FnMut(Event) + 'static) -> Self {
- self.on_press = Some(Box::new(handler));
+ pub fn on_press(mut self, handler: impl Into>>) -> Self {
+ self.on_press = Some(handler.into());
self
}
}
@@ -147,46 +147,79 @@ impl IntoElement for TabBar {
.cross_align(Alignment::Center)
.content(Content::Fit)
.children(self.tabs.into_iter().map(|tab| {
- let active = tab.active;
-
- let mut el = rect()
- .vertical()
- .content(Content::Fit)
- .on_pointer_enter(|_| Cursor::set(CursorIcon::Pointer))
- .on_pointer_leave(|_| Cursor::set(CursorIcon::default()));
-
- if let Some(handler) = tab.on_press {
- el = el.on_press(handler);
+ TabButton {
+ label: tab.label,
+ active: tab.active,
+ count: tab.count,
+ font_size,
+ on_press: tab.on_press,
}
-
- el.child(
- rect()
- .horizontal()
- .cross_align(Alignment::Center)
- .spacing(6.)
- .content(Content::Fit)
- .child(
- rect()
- .vertical()
- .content(Content::Fit)
- .child(tab_label(&tab.label, font_size, active))
- .child(
- rect()
- .height(Size::px(1.5))
- .width(Size::fill_minimum())
- .margin(Gaps::new_symmetric(0., 4.0))
- .corner_radius(CornerRadius::new_all(2.))
- .background(if active {
- colors::fg_primary()
- } else {
- Color::TRANSPARENT
- }),
- ),
- )
- .maybe_child(tab.count.map(|c| count_pill(&c, font_size).into_element())),
- )
.into_element()
}))
.into_element()
}
}
+
+#[derive(PartialEq)]
+struct TabButton {
+ label: String,
+ active: bool,
+ count: Option,
+ font_size: f32,
+ on_press: Option>>,
+}
+
+impl Component for TabButton {
+ fn render(&self) -> impl IntoElement {
+ let a11y_id = use_a11y();
+ let focus = use_focus(a11y_id);
+
+ let active = self.active;
+ let font_size = self.font_size;
+ let underline_on = active || focus().is_focused();
+
+ let mut el = rect()
+ .vertical()
+ .content(Content::Fit)
+ .a11y_id(a11y_id)
+ .a11y_focusable(true)
+ .a11y_role(AccessibilityRole::Button)
+ .on_pointer_enter(|_| Cursor::set(CursorIcon::Pointer))
+ .on_pointer_leave(|_| Cursor::set(CursorIcon::default()));
+
+ if let Some(handler) = self.on_press.clone() {
+ el = el.on_all_press(move |e| handler.call(e));
+ }
+
+ el.child(
+ rect()
+ .horizontal()
+ .cross_align(Alignment::Center)
+ .spacing(6.)
+ .content(Content::Fit)
+ .child(
+ rect()
+ .vertical()
+ .content(Content::Fit)
+ .child(tab_label(&self.label, font_size, active))
+ .child(
+ rect()
+ .height(Size::px(1.5))
+ .width(Size::fill_minimum())
+ .margin(Gaps::new_symmetric(0., 4.0))
+ .corner_radius(CornerRadius::new_all(2.))
+ .background(if underline_on {
+ colors::fg_primary()
+ } else {
+ Color::TRANSPARENT
+ }),
+ ),
+ )
+ .maybe_child(
+ self.count
+ .as_ref()
+ .map(|c| count_pill(c, font_size).into_element()),
+ ),
+ )
+ }
+}
diff --git a/packages/oneclient_app/src/components/toggle.rs b/packages/oneclient_app/src/components/toggle.rs
index eef1b4ba..453ecdd4 100644
--- a/packages/oneclient_app/src/components/toggle.rs
+++ b/packages/oneclient_app/src/components/toggle.rs
@@ -50,8 +50,17 @@ impl Component for Switch {
let on_press = self.on_press.clone();
let on = *value.read();
+ let a11y_id = use_a11y();
+ let focus = use_focus(a11y_id);
+
let knob_align = if on { Alignment::End } else { Alignment::Start };
+ let track_border = if focus().is_focused() {
+ colors::fg_primary()
+ } else {
+ colors::component_border()
+ };
+
let background = use_animation_transition(value, |_, on| {
if on {
AnimColor::new(colors::component_bg(), colors::brand())
@@ -84,9 +93,12 @@ impl Component for Switch {
.horizontal()
.cross_align(Alignment::Center)
.spacing(8.)
+ .a11y_id(a11y_id)
+ .a11y_focusable(true)
+ .a11y_role(AccessibilityRole::Button)
.on_pointer_enter(|_| Cursor::set(CursorIcon::Pointer))
.on_pointer_leave(|_| Cursor::set(CursorIcon::default()))
- .on_press(move |_| {
+ .on_all_press(move |_| {
on_press.call(());
})
.child(
@@ -104,7 +116,7 @@ impl Component for Switch {
.main_align(knob_align)
.cross_align(Alignment::Center)
.background(&*background.read())
- .border(ui::border_all_color(1., colors::component_border()))
+ .border(ui::border_all_color(1., track_border))
.child(
rect()
.width(Size::px(HANDLE_SIZE))
diff --git a/packages/oneclient_app/src/layout/settings_shell.rs b/packages/oneclient_app/src/layout/settings_shell.rs
index b9d196cb..773be6c1 100644
--- a/packages/oneclient_app/src/layout/settings_shell.rs
+++ b/packages/oneclient_app/src/layout/settings_shell.rs
@@ -717,6 +717,9 @@ impl Component for SidebarItem {
fn render(&self) -> impl IntoElement {
let mut hovering = use_state(|| false);
+ let a11y_id = use_a11y();
+ let focus = use_focus(a11y_id);
+
let tab = self.tab;
let active = self.active;
let route = tab.route();
@@ -731,7 +734,7 @@ impl Component for SidebarItem {
let has_dot = matches!(tab, SettingsTab::Changelog);
- rect()
+ let mut el = rect()
.horizontal()
.content(Content::Flex)
.width(Size::fill())
@@ -741,6 +744,9 @@ impl Component for SidebarItem {
.padding(Gaps::new_symmetric(0., 12.))
.corner_radius(CornerRadius::new_all(7.))
.background(background)
+ .a11y_id(a11y_id)
+ .a11y_focusable(true)
+ .a11y_role(AccessibilityRole::Button)
.on_pointer_enter(move |_| {
*hovering.write() = true;
Cursor::set(CursorIcon::Pointer);
@@ -750,11 +756,21 @@ impl Component for SidebarItem {
Cursor::set(CursorIcon::default());
})
.map(route, |el, route| {
- el.on_press(move |_| {
+ el.on_all_press(move |_| {
let _ = RouterContext::get().push(route.clone());
})
- })
- .child(Icon::new(tab.icon()).size(18.).color(colors::fg_primary()))
+ });
+
+ if focus().is_focused() {
+ el = el.border(
+ Border::new()
+ .fill(colors::brand())
+ .width(1.)
+ .alignment(BorderAlignment::Inner),
+ );
+ }
+
+ el.child(Icon::new(tab.icon()).size(18.).color(colors::fg_primary()))
.child(
rect()
.horizontal()
diff --git a/packages/oneclient_app/src/view/app/browser/index/cards.rs b/packages/oneclient_app/src/view/app/browser/index/cards.rs
index f734b8eb..bc9c10cb 100644
--- a/packages/oneclient_app/src/view/app/browser/index/cards.rs
+++ b/packages/oneclient_app/src/view/app/browser/index/cards.rs
@@ -66,6 +66,10 @@ impl Component for PackageCard {
let icon_url = self.item.icon_url.clone();
let cluster_id = self.cluster_id;
+ let a11y_id = use_a11y();
+ let focus = use_focus(a11y_id);
+ let focused = focus().is_focused();
+
use_drop(|| {
Cursor::set(CursorIcon::default());
});
@@ -76,11 +80,21 @@ impl Component for PackageCard {
.height(Size::px(CARD_H))
.corner_radius(CornerRadius::new_all(10.))
.background(CARD_BG)
- .border(border_all_color(1., colors::component_border()))
+ .border(border_all_color(
+ 1.,
+ if focused {
+ colors::brand()
+ } else {
+ colors::component_border()
+ },
+ ))
.overflow(Overflow::Clip)
+ .a11y_id(a11y_id)
+ .a11y_focusable(true)
+ .a11y_role(AccessibilityRole::Button)
.on_pointer_enter(|_| Cursor::set(CursorIcon::Pointer))
.on_pointer_leave(|_| Cursor::set(CursorIcon::default()))
- .on_press(move |_| open_package(cluster_id, &package_type, provider, &id))
+ .on_all_press(move |_| open_package(cluster_id, &package_type, provider, &id))
.child(
rect()
.margin(Gaps::new_all(1.))
@@ -141,9 +155,35 @@ impl Component for PackageCard {
}
pub(super) fn list_row(item: ProjectSummary, cluster_id: i64, package_type: &str) -> impl IntoElement {
- let id = item.id.clone();
- let provider = item.provider;
- let package_type = package_type.to_string();
+ ListRow {
+ item,
+ cluster_id,
+ package_type: package_type.to_string(),
+ }
+}
+
+#[derive(PartialEq)]
+struct ListRow {
+ item: ProjectSummary,
+ cluster_id: i64,
+ package_type: String,
+}
+
+impl Component for ListRow {
+ fn render(&self) -> impl IntoElement {
+ let item = self.item.clone();
+ let cluster_id = self.cluster_id;
+ let id = item.id.clone();
+ let provider = item.provider;
+ let package_type = self.package_type.clone();
+
+ let a11y_id = use_a11y();
+ let focus = use_focus(a11y_id);
+ let focused = focus().is_focused();
+
+ use_drop(|| {
+ Cursor::set(CursorIcon::default());
+ });
rect()
.horizontal()
@@ -154,12 +194,22 @@ pub(super) fn list_row(item: ProjectSummary, cluster_id: i64, package_type: &str
.padding(Gaps::new_all(16.))
.corner_radius(CornerRadius::new_all(10.))
.background(CARD_BG)
- .border(border_all_color(1., colors::component_border()))
+ .border(border_all_color(
+ 1.,
+ if focused {
+ colors::brand()
+ } else {
+ colors::component_border()
+ },
+ ))
.overflow(Overflow::Clip)
.content(Content::Flex)
+ .a11y_id(a11y_id)
+ .a11y_focusable(true)
+ .a11y_role(AccessibilityRole::Button)
.on_pointer_enter(|_| Cursor::set(CursorIcon::Pointer))
.on_pointer_leave(|_| Cursor::set(CursorIcon::default()))
- .on_press(move |_| open_package(cluster_id, &package_type, provider, &id))
+ .on_all_press(move |_| open_package(cluster_id, &package_type, provider, &id))
.child(Thumbnail::new(item.icon_url.clone(), 48.).radius(8.))
.child(
rect()
@@ -198,6 +248,7 @@ pub(super) fn list_row(item: ProjectSummary, cluster_id: i64, package_type: &str
),
)
.child(downloads_row(item.downloads))
+ }
}
fn downloads_row(downloads: u64) -> impl IntoElement {
diff --git a/packages/oneclient_app/src/view/app/browser/index/sidebar.rs b/packages/oneclient_app/src/view/app/browser/index/sidebar.rs
index 5faa471f..1e03d381 100644
--- a/packages/oneclient_app/src/view/app/browser/index/sidebar.rs
+++ b/packages/oneclient_app/src/view/app/browser/index/sidebar.rs
@@ -77,10 +77,15 @@ impl KeyExt for CategoryRow {
impl Component for CategoryRow {
fn render(&self) -> impl IntoElement {
let mut hovering = use_state(|| false);
+
+ let a11y_id = use_a11y();
+ let focus = use_focus(a11y_id);
+
let selected = self.selected;
let on_toggle = self.on_toggle.clone();
let hovered = *hovering.read();
- let color = if selected || hovered {
+ let focused = focus().is_focused();
+ let color = if selected || hovered || focused {
colors::fg_primary()
} else {
colors::fg_secondary()
@@ -89,6 +94,9 @@ impl Component for CategoryRow {
rect()
.width(Size::fill())
.padding(Gaps::new_symmetric(3., 0.))
+ .a11y_id(a11y_id)
+ .a11y_focusable(true)
+ .a11y_role(AccessibilityRole::Button)
.on_pointer_enter(move |_| {
hovering.set(true);
Cursor::set(CursorIcon::Pointer);
@@ -97,7 +105,7 @@ impl Component for CategoryRow {
hovering.set(false);
Cursor::set(CursorIcon::default());
})
- .on_press(move |_| on_toggle.call(()))
+ .on_all_press(move |_| on_toggle.call(()))
.child(
label()
.text(capitalize(&self.name))
diff --git a/packages/oneclient_app/src/view/app/browser/package/mod.rs b/packages/oneclient_app/src/view/app/browser/package/mod.rs
index bfee57af..db7aadd6 100644
--- a/packages/oneclient_app/src/view/app/browser/package/mod.rs
+++ b/packages/oneclient_app/src/view/app/browser/package/mod.rs
@@ -184,7 +184,7 @@ impl Component for BrowserPackage {
.vertical()
.width(Size::flex(1.0))
.spacing(12.)
- .child(tabs(current, active_tab, has_gallery))
+ .child(tabs(active_tab, has_gallery))
.child(body),
)
.into_element();
diff --git a/packages/oneclient_app/src/view/app/browser/package/panels.rs b/packages/oneclient_app/src/view/app/browser/package/panels.rs
index e054105f..c858f419 100644
--- a/packages/oneclient_app/src/view/app/browser/package/panels.rs
+++ b/packages/oneclient_app/src/view/app/browser/package/panels.rs
@@ -6,7 +6,7 @@ use oneclient_core::packages::types::{
use oneclient_core::packages::ProviderId;
use crate::BridgeDispatch;
-use crate::components::{Button, Icon, IconType};
+use crate::components::{Button, Icon, IconType, Segment, SegmentedControl};
use crate::hooks::VERSIONS_PAGE_SIZE;
use crate::theme::colors;
use crate::ui::border_all_color;
@@ -27,40 +27,15 @@ pub(super) fn loading_body() -> impl IntoElement {
.into_element()
}
-pub(super) fn tabs(current: usize, active_tab: State, has_gallery: bool) -> impl IntoElement {
- let mut labels: Vec<&str> = vec!["About", "Versions"];
+pub(super) fn tabs(active_tab: State, has_gallery: bool) -> impl IntoElement {
+ let mut control = SegmentedControl::new(active_tab)
+ .height(36.)
+ .segment(Segment::new(0usize).label("About"))
+ .segment(Segment::new(1usize).label("Versions"));
if has_gallery {
- labels.push("Gallery");
+ control = control.segment(Segment::new(2usize).label("Gallery"));
}
- rect()
- .horizontal()
- .spacing(6.)
- .padding(Gaps::new_all(4.))
- .corner_radius(CornerRadius::new_all(9.))
- .background(colors::component_bg())
- .children(labels.into_iter().enumerate().map(move |(i, t)| {
- let selected = i == current;
- let mut active_tab = active_tab;
- rect()
- .center()
- .padding(Gaps::new_symmetric(6., 16.))
- .corner_radius(CornerRadius::new_all(7.))
- .background(if selected {
- colors::component_bg_pressed()
- } else {
- Color::TRANSPARENT
- })
- .on_pointer_enter(|_| Cursor::set(CursorIcon::Pointer))
- .on_pointer_leave(|_| Cursor::set(CursorIcon::default()))
- .on_press(move |_| *active_tab.write() = i)
- .child(label().text(t).font_size(13.).color(if selected {
- colors::fg_primary()
- } else {
- colors::fg_secondary()
- }))
- .into_element()
- }))
- .into_element()
+ control
}
pub(super) fn about_panel(project: &ProjectDetail) -> impl IntoElement {
diff --git a/packages/oneclient_app/src/view/app/browser/package/sidebar.rs b/packages/oneclient_app/src/view/app/browser/package/sidebar.rs
index 3543eb9a..3c4e40fc 100644
--- a/packages/oneclient_app/src/view/app/browser/package/sidebar.rs
+++ b/packages/oneclient_app/src/view/app/browser/package/sidebar.rs
@@ -174,44 +174,78 @@ fn authors_card(project: &ProjectDetail, confirm: State