feat(ui): Make the launcher navigatable with the keyboard
This commit is contained in:
parent
21313b2093
commit
557b0e4b7b
18 changed files with 735 additions and 399 deletions
|
|
@ -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::<Area>);
|
||||
let mut list_size = use_state(|| None::<Size2D>);
|
||||
|
||||
|
|
@ -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<SizedEventData>| {
|
||||
button_area.set_if_modified(Some(e.area));
|
||||
})
|
||||
.on_press(move |e: Event<PressEventData>| {
|
||||
.on_all_press(move |e: Event<PressEventData>| {
|
||||
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())
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -311,36 +311,42 @@ impl ScrollArea {
|
|||
let can_scroll_h = horizontal && vp_w > 0. && vp_w < content_w;
|
||||
|
||||
let on_wheel = move |e: Event<WheelEventData>| {
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -101,16 +101,6 @@ impl<T: Copy + PartialEq + 'static> Component for SegmentedControl<T> {
|
|||
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<T: Copy + PartialEq + 'static> Component for SegmentedControl<T> {
|
|||
.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<T: Copy + PartialEq + 'static> {
|
||||
selected: State<T>,
|
||||
value: T,
|
||||
is_selected: bool,
|
||||
icon: Option<IconType>,
|
||||
label: Option<String>,
|
||||
icon_size: Option<f32>,
|
||||
tint: bool,
|
||||
equal_width: Option<f32>,
|
||||
disabled: bool,
|
||||
}
|
||||
|
||||
impl<T: Copy + PartialEq + 'static> Component for SegmentButton<T> {
|
||||
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()
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ pub struct TabItem {
|
|||
label: String,
|
||||
active: bool,
|
||||
count: Option<String>,
|
||||
on_press: Option<Box<dyn FnMut(Event<PressEventData>)>>,
|
||||
on_press: Option<EventHandler<Event<PressEventData>>>,
|
||||
}
|
||||
|
||||
impl TabItem {
|
||||
|
|
@ -25,8 +25,8 @@ impl TabItem {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn on_press(mut self, handler: impl FnMut(Event<PressEventData>) + 'static) -> Self {
|
||||
self.on_press = Some(Box::new(handler));
|
||||
pub fn on_press(mut self, handler: impl Into<EventHandler<Event<PressEventData>>>) -> 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<String>,
|
||||
font_size: f32,
|
||||
on_press: Option<EventHandler<Event<PressEventData>>>,
|
||||
}
|
||||
|
||||
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()),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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<usize>, has_gallery: bool) -> impl IntoElement {
|
||||
let mut labels: Vec<&str> = vec!["About", "Versions"];
|
||||
pub(super) fn tabs(active_tab: State<usize>, 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 {
|
||||
|
|
|
|||
|
|
@ -174,44 +174,78 @@ fn authors_card(project: &ProjectDetail, confirm: State<Option<String>>) -> impl
|
|||
card("Authors", rows)
|
||||
}
|
||||
|
||||
fn author_row(member: &ProjectMember, mut confirm: State<Option<String>>) -> Element {
|
||||
let url = member.url.clone();
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(8.)
|
||||
.corner_radius(CornerRadius::new_all(6.))
|
||||
.padding(Gaps::new_all(4.))
|
||||
.maybe(url.is_some(), |el| {
|
||||
let url = url.clone().unwrap_or_default();
|
||||
el.on_pointer_enter(|_| Cursor::set(CursorIcon::Pointer))
|
||||
.on_pointer_leave(|_| Cursor::set(CursorIcon::default()))
|
||||
.on_press(move |_| confirm.set(Some(url.clone())))
|
||||
})
|
||||
.child(Thumbnail::new(member.avatar_url.clone(), 32.).radius(6.))
|
||||
.child(
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::flex(1.0))
|
||||
.child(
|
||||
label()
|
||||
.text(member.name.clone())
|
||||
.font_size(12.)
|
||||
.max_lines(1)
|
||||
.color(colors::fg_primary()),
|
||||
)
|
||||
.maybe(!member.role.is_empty(), |el| {
|
||||
el.child(
|
||||
fn author_row(member: &ProjectMember, confirm: State<Option<String>>) -> Element {
|
||||
AuthorRow {
|
||||
name: member.name.clone(),
|
||||
role: member.role.clone(),
|
||||
url: member.url.clone(),
|
||||
avatar_url: member.avatar_url.clone(),
|
||||
confirm,
|
||||
}
|
||||
.into_element()
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct AuthorRow {
|
||||
name: String,
|
||||
role: String,
|
||||
url: Option<String>,
|
||||
avatar_url: Option<String>,
|
||||
confirm: State<Option<String>>,
|
||||
}
|
||||
|
||||
impl Component for AuthorRow {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let a11y_id = use_a11y();
|
||||
let focus = use_focus(a11y_id);
|
||||
let focused = focus().is_focused();
|
||||
|
||||
let url = self.url.clone();
|
||||
let interactive = url.is_some();
|
||||
let mut confirm = self.confirm;
|
||||
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(8.)
|
||||
.corner_radius(CornerRadius::new_all(6.))
|
||||
.padding(Gaps::new_all(4.))
|
||||
.maybe(interactive, |el| {
|
||||
let url = url.clone().unwrap_or_default();
|
||||
el.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_all_press(move |_| confirm.set(Some(url.clone())))
|
||||
})
|
||||
.maybe(interactive && focused, |el| {
|
||||
el.border(border_all_color(1., colors::brand()))
|
||||
})
|
||||
.child(Thumbnail::new(self.avatar_url.clone(), 32.).radius(6.))
|
||||
.child(
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::flex(1.0))
|
||||
.child(
|
||||
label()
|
||||
.text(member.role.clone())
|
||||
.font_size(10.)
|
||||
.text(self.name.clone())
|
||||
.font_size(12.)
|
||||
.max_lines(1)
|
||||
.color(colors::fg_secondary()),
|
||||
.color(colors::fg_primary()),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.into_element()
|
||||
.maybe(!self.role.is_empty(), |el| {
|
||||
el.child(
|
||||
label()
|
||||
.text(self.role.clone())
|
||||
.font_size(10.)
|
||||
.max_lines(1)
|
||||
.color(colors::fg_secondary()),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn details_card(project: &ProjectDetail) -> impl IntoElement {
|
||||
|
|
@ -282,25 +316,57 @@ fn links_card(links: &[(String, String)], confirm: State<Option<String>>) -> imp
|
|||
card("Links", rows)
|
||||
}
|
||||
|
||||
fn link_row(label_text: String, url: String, mut confirm: State<Option<String>>) -> Element {
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(8.)
|
||||
.on_pointer_enter(|_| Cursor::set(CursorIcon::Pointer))
|
||||
.on_pointer_leave(|_| Cursor::set(CursorIcon::default()))
|
||||
.on_press(move |_| confirm.set(Some(url.clone())))
|
||||
.child(
|
||||
Icon::new(IconType::Link03)
|
||||
.size(14.)
|
||||
.color(colors::code_info()),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text(label_text)
|
||||
.font_size(12.)
|
||||
.color(colors::code_info()),
|
||||
)
|
||||
.into_element()
|
||||
fn link_row(label_text: String, url: String, confirm: State<Option<String>>) -> Element {
|
||||
LinkRow {
|
||||
label_text,
|
||||
url,
|
||||
confirm,
|
||||
}
|
||||
.into_element()
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct LinkRow {
|
||||
label_text: String,
|
||||
url: String,
|
||||
confirm: State<Option<String>>,
|
||||
}
|
||||
|
||||
impl Component for LinkRow {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let a11y_id = use_a11y();
|
||||
let focus = use_focus(a11y_id);
|
||||
let focused = focus().is_focused();
|
||||
|
||||
let url = self.url.clone();
|
||||
let mut confirm = self.confirm;
|
||||
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(8.)
|
||||
.corner_radius(CornerRadius::new_all(6.))
|
||||
.padding(Gaps::new_all(4.))
|
||||
.a11y_id(a11y_id)
|
||||
.a11y_focusable(true)
|
||||
.a11y_role(AccessibilityRole::Button)
|
||||
.maybe(focused, |el| {
|
||||
el.border(border_all_color(1., colors::brand()))
|
||||
})
|
||||
.on_pointer_enter(|_| Cursor::set(CursorIcon::Pointer))
|
||||
.on_pointer_leave(|_| Cursor::set(CursorIcon::default()))
|
||||
.on_all_press(move |_| confirm.set(Some(url.clone())))
|
||||
.child(
|
||||
Icon::new(IconType::Link03)
|
||||
.size(14.)
|
||||
.color(colors::code_info()),
|
||||
)
|
||||
.child(
|
||||
label()
|
||||
.text(self.label_text.clone())
|
||||
.font_size(12.)
|
||||
.color(colors::code_info()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use oneclient_core::packages::domain::GameLoader;
|
|||
use oneclient_core::settings::{GameSettingsProfile, ProfileUpdate, Resolution};
|
||||
|
||||
use crate::components::{
|
||||
Dropdown, Icon, IconType, ScrollArea, TextInput, toggle, toggle_controlled, validate_number,
|
||||
Button, Dropdown, Icon, IconType, ScrollArea, TextInput, toggle, toggle_controlled, validate_number
|
||||
};
|
||||
use crate::hooks::{
|
||||
ClusterAction, java_runtimes, loader_versions, try_game_profile, use_cluster_mutation,
|
||||
|
|
@ -147,16 +147,15 @@ fn reset_button(overridden: bool, on_reset: EventHandler<()>) -> impl IntoElemen
|
|||
colors::fg_secondary().with_a(60)
|
||||
};
|
||||
|
||||
rect()
|
||||
.center()
|
||||
.width(Size::px(30.))
|
||||
.height(Size::px(30.))
|
||||
Button::new()
|
||||
.small()
|
||||
.ghost()
|
||||
.icon()
|
||||
.corner_radius(CornerRadius::new_all(7.))
|
||||
.maybe(overridden, |el| {
|
||||
el.on_pointer_enter(|_| Cursor::set(CursorIcon::Pointer))
|
||||
.on_pointer_leave(|_| Cursor::set(CursorIcon::default()))
|
||||
.on_press(move |_| on_reset.call(()))
|
||||
el.on_press(move |_| on_reset.call(()))
|
||||
})
|
||||
.enabled(overridden)
|
||||
.child(Icon::new(IconType::RefreshCcw02).size(15.).color(color))
|
||||
.into_element()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -355,6 +355,9 @@ impl Component for LogPicker {
|
|||
let mut open = use_state(|| false);
|
||||
let filter = use_state(String::new);
|
||||
|
||||
let a11y_id = use_a11y();
|
||||
let focus = use_focus(a11y_id);
|
||||
|
||||
let enabled = self.enabled;
|
||||
let label_text = self.label.clone();
|
||||
let files = self.files.clone();
|
||||
|
|
@ -400,14 +403,24 @@ impl Component for LogPicker {
|
|||
.padding(Gaps::new_symmetric(0., 12.))
|
||||
.corner_radius(CornerRadius::new_all(9.))
|
||||
.background(colors::component_bg())
|
||||
.border(ui::border_all_color(1., colors::component_border()))
|
||||
.a11y_id(a11y_id)
|
||||
.a11y_focusable(enabled)
|
||||
.a11y_role(AccessibilityRole::Button)
|
||||
.border(ui::border_all_color(
|
||||
1.,
|
||||
if enabled && focus().is_focused() {
|
||||
colors::brand()
|
||||
} else {
|
||||
colors::component_border()
|
||||
},
|
||||
))
|
||||
.on_pointer_enter(move |_| {
|
||||
if enabled {
|
||||
Cursor::set(CursorIcon::Pointer)
|
||||
}
|
||||
})
|
||||
.on_pointer_leave(|_| Cursor::set(CursorIcon::default()))
|
||||
.on_press(move |e: Event<PressEventData>| {
|
||||
.on_all_press(move |e: Event<PressEventData>| {
|
||||
if !enabled {
|
||||
return;
|
||||
}
|
||||
|
|
@ -495,6 +508,9 @@ impl Component for PickerRow {
|
|||
let mut hovering = use_state(|| false);
|
||||
let file = self.file.clone();
|
||||
|
||||
let a11y_id = use_a11y();
|
||||
let focus = use_focus(a11y_id);
|
||||
|
||||
rect()
|
||||
.width(Size::fill())
|
||||
.horizontal()
|
||||
|
|
@ -503,7 +519,10 @@ impl Component for PickerRow {
|
|||
.spacing(8.)
|
||||
.padding(Gaps::new_symmetric(6., 8.))
|
||||
.corner_radius(CornerRadius::new_all(7.))
|
||||
.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
|
||||
|
|
@ -516,7 +535,7 @@ impl Component for PickerRow {
|
|||
hovering.set(false);
|
||||
Cursor::set(CursorIcon::default());
|
||||
})
|
||||
.on_press(self.on_press.clone())
|
||||
.on_all_press(self.on_press.clone())
|
||||
.child(
|
||||
rect()
|
||||
.vertical()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use super::*;
|
||||
|
||||
use freya::animation::{AnimNum, Ease, OnCreation, use_animation};
|
||||
use freya::router::RouterContext;
|
||||
use oneclient_core::packages::ContentType;
|
||||
use oneclient_core::settings::ViewLayout;
|
||||
|
|
@ -184,7 +185,6 @@ struct FilterButton {
|
|||
impl Component for FilterButton {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let mut open = use_state(|| false);
|
||||
let mut hovering = use_state(|| false);
|
||||
|
||||
let sort = self.sort;
|
||||
let current_sort = self.current_sort;
|
||||
|
|
@ -194,11 +194,6 @@ impl Component for FilterButton {
|
|||
let active =
|
||||
current_sort != SortMode::NameAsc || *enabled_filter.read() != EnabledFilter::All;
|
||||
|
||||
let bg = if is_open || hovering() {
|
||||
colors::component_bg_hover()
|
||||
} else {
|
||||
colors::component_bg()
|
||||
};
|
||||
let icon_color = if active {
|
||||
colors::brand()
|
||||
} else {
|
||||
|
|
@ -208,21 +203,11 @@ impl Component for FilterButton {
|
|||
rect()
|
||||
.width(Size::px(FILTER_BTN_W))
|
||||
.child(
|
||||
rect()
|
||||
Button::new()
|
||||
.secondary()
|
||||
.icon()
|
||||
.width(Size::px(FILTER_BTN_W))
|
||||
.height(Size::px(34.))
|
||||
.center()
|
||||
.corner_radius(CornerRadius::new_all(8.))
|
||||
.background(bg)
|
||||
.border(crate::ui::border_all_color(1., colors::component_border()))
|
||||
.on_pointer_enter(move |_| {
|
||||
hovering.set(true);
|
||||
Cursor::set(CursorIcon::Pointer);
|
||||
})
|
||||
.on_pointer_leave(move |_| {
|
||||
hovering.set(false);
|
||||
Cursor::set(CursorIcon::default());
|
||||
})
|
||||
.on_press(move |e: Event<PressEventData>| {
|
||||
e.stop_propagation();
|
||||
open.toggle();
|
||||
|
|
@ -230,67 +215,112 @@ impl Component for FilterButton {
|
|||
.child(Icon::new(IconType::Sliders04).size(16.).color(icon_color)),
|
||||
)
|
||||
.maybe_child(is_open.then(|| {
|
||||
filter_popover(sort, current_sort, enabled_filter, move || open.set(false))
|
||||
.into_element()
|
||||
let on_close: EventHandler<()> = (move |()| open.set(false)).into();
|
||||
FilterPopover {
|
||||
sort,
|
||||
current_sort,
|
||||
enabled_filter,
|
||||
on_close,
|
||||
}
|
||||
.into_element()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn filter_popover(
|
||||
mut sort: State<Option<String>>,
|
||||
#[derive(PartialEq)]
|
||||
struct FilterPopover {
|
||||
sort: State<Option<String>>,
|
||||
current_sort: SortMode,
|
||||
mut enabled_filter: State<EnabledFilter>,
|
||||
on_close: impl FnMut() + Clone + 'static,
|
||||
) -> impl IntoElement {
|
||||
let mut close_backdrop = on_close.clone();
|
||||
enabled_filter: State<EnabledFilter>,
|
||||
on_close: EventHandler<()>,
|
||||
}
|
||||
|
||||
let show = *enabled_filter.read();
|
||||
impl Component for FilterPopover {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let mut sort = self.sort;
|
||||
let current_sort = self.current_sort;
|
||||
let mut enabled_filter = self.enabled_filter;
|
||||
let backdrop_close = self.on_close.clone();
|
||||
let key_close = self.on_close.clone();
|
||||
|
||||
let mut panel = rect()
|
||||
.vertical()
|
||||
.width(Size::fill())
|
||||
.spacing(4.)
|
||||
.padding(Gaps::new_all(8.))
|
||||
.corner_radius(CornerRadius::new_all(10.))
|
||||
.border(crate::ui::border_all_color(1., colors::component_border()))
|
||||
.background(colors::page_elevated())
|
||||
.child(section_label("Sort by"));
|
||||
let a11y_id = use_a11y();
|
||||
|
||||
for mode in SortMode::ALL {
|
||||
let selected = mode == current_sort;
|
||||
panel = panel.child(choice_row(mode.label(), selected, move |_| {
|
||||
sort.set(Some(mode.key().to_string()));
|
||||
}));
|
||||
let fade = use_animation(|conf| {
|
||||
conf.on_creation(OnCreation::Run);
|
||||
AnimNum::new(0., 1.).time(160).ease(Ease::Out)
|
||||
});
|
||||
let progress = fade.read().value();
|
||||
|
||||
let show = *enabled_filter.read();
|
||||
|
||||
let mut panel = rect()
|
||||
.vertical()
|
||||
.width(Size::fill())
|
||||
.spacing(4.)
|
||||
.padding(Gaps::new_all(8.))
|
||||
.opacity(progress)
|
||||
.offset_y((progress - 1.0) * 6.)
|
||||
.corner_radius(CornerRadius::new_all(10.))
|
||||
.background(colors::page_elevated().with_a(230))
|
||||
.blur(12.)
|
||||
.border(crate::ui::border_all_color(1., colors::component_border()))
|
||||
.shadow(Shadow::from((0., 8., 32., 0., Color::from_argb(120, 0, 0, 0))))
|
||||
.a11y_id(a11y_id)
|
||||
.a11y_role(AccessibilityRole::Dialog)
|
||||
.on_global_key_down(move |e: Event<KeyboardEventData>| {
|
||||
if e.key == Key::Named(NamedKey::Escape) {
|
||||
key_close.call(());
|
||||
}
|
||||
})
|
||||
.child(section_label("Sort by"));
|
||||
|
||||
for mode in SortMode::ALL {
|
||||
let selected = mode == current_sort;
|
||||
let on_press: EventHandler<Event<PressEventData>> = (move |_| {
|
||||
sort.set(Some(mode.key().to_string()));
|
||||
})
|
||||
.into();
|
||||
panel = panel.child(ChoiceRow {
|
||||
text: mode.label(),
|
||||
selected,
|
||||
on_press,
|
||||
});
|
||||
}
|
||||
|
||||
panel = panel.child(section_label("Show"));
|
||||
for filter in EnabledFilter::ALL {
|
||||
let selected = filter == show;
|
||||
let on_press: EventHandler<Event<PressEventData>> = (move |_| {
|
||||
enabled_filter.set(filter);
|
||||
})
|
||||
.into();
|
||||
panel = panel.child(ChoiceRow {
|
||||
text: filter.label(),
|
||||
selected,
|
||||
on_press,
|
||||
});
|
||||
}
|
||||
|
||||
rect()
|
||||
.height(Size::px(0.))
|
||||
.width(Size::px(FILTER_PANEL_W))
|
||||
.layer(Layer::Overlay)
|
||||
.child(
|
||||
rect()
|
||||
.layer(Layer::RelativeOverlay(10))
|
||||
.position(Position::new_global().top(0.).left(0.))
|
||||
.width(Size::window_percent(100.))
|
||||
.height(Size::window_percent(100.))
|
||||
.on_press(move |_| backdrop_close.call(())),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::fill())
|
||||
.layer(Layer::RelativeOverlay(12))
|
||||
.margin(Gaps::new(6., 0., 0., -(FILTER_PANEL_W - FILTER_BTN_W)))
|
||||
.child(panel),
|
||||
)
|
||||
}
|
||||
|
||||
panel = panel.child(section_label("Show"));
|
||||
for filter in EnabledFilter::ALL {
|
||||
let selected = filter == show;
|
||||
panel = panel.child(choice_row(filter.label(), selected, move |_| {
|
||||
enabled_filter.set(filter);
|
||||
}));
|
||||
}
|
||||
|
||||
rect()
|
||||
.height(Size::px(0.))
|
||||
.width(Size::px(FILTER_PANEL_W))
|
||||
.layer(Layer::Overlay)
|
||||
.child(
|
||||
rect()
|
||||
.layer(Layer::RelativeOverlay(10))
|
||||
.position(Position::new_global().top(0.).left(0.))
|
||||
.width(Size::window_percent(100.))
|
||||
.height(Size::window_percent(100.))
|
||||
.on_press(move |_| close_backdrop()),
|
||||
)
|
||||
.child(
|
||||
rect()
|
||||
.width(Size::fill())
|
||||
.layer(Layer::RelativeOverlay(12))
|
||||
.margin(Gaps::new(6., 0., 0., -(FILTER_PANEL_W - FILTER_BTN_W)))
|
||||
.child(panel),
|
||||
)
|
||||
.into_element()
|
||||
}
|
||||
|
||||
fn section_label(text: &'static str) -> impl IntoElement {
|
||||
|
|
@ -301,45 +331,68 @@ fn section_label(text: &'static str) -> impl IntoElement {
|
|||
.color(colors::fg_secondary())
|
||||
}
|
||||
|
||||
fn choice_row(
|
||||
#[derive(PartialEq)]
|
||||
struct ChoiceRow {
|
||||
text: &'static str,
|
||||
selected: bool,
|
||||
on_press: impl FnMut(Event<PressEventData>) + 'static,
|
||||
) -> impl IntoElement {
|
||||
let color = if selected {
|
||||
colors::fg_primary()
|
||||
} else {
|
||||
colors::fg_secondary()
|
||||
};
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(8.)
|
||||
.padding(Gaps::new_symmetric(5., 8.))
|
||||
.corner_radius(CornerRadius::new_all(6.))
|
||||
.background(if selected {
|
||||
on_press: EventHandler<Event<PressEventData>>,
|
||||
}
|
||||
|
||||
impl Component for ChoiceRow {
|
||||
fn render(&self) -> impl IntoElement {
|
||||
let text = self.text;
|
||||
let selected = self.selected;
|
||||
|
||||
let a11y_id = use_a11y();
|
||||
let focus = use_focus(a11y_id);
|
||||
let focused = focus().is_focused();
|
||||
|
||||
let color = if selected {
|
||||
colors::fg_primary()
|
||||
} else {
|
||||
colors::fg_secondary()
|
||||
};
|
||||
|
||||
let bg = if selected {
|
||||
colors::component_bg()
|
||||
} else if focused {
|
||||
colors::ghost_overlay_hover()
|
||||
} else {
|
||||
Color::TRANSPARENT
|
||||
})
|
||||
.content(Content::Flex)
|
||||
.on_pointer_enter(|_| Cursor::set(CursorIcon::Pointer))
|
||||
.on_pointer_leave(|_| Cursor::set(CursorIcon::default()))
|
||||
.on_press(on_press)
|
||||
.child(
|
||||
label()
|
||||
.text(text)
|
||||
.font_size(12.)
|
||||
.width(Size::flex(1.0))
|
||||
.color(color),
|
||||
)
|
||||
.maybe_child(selected.then(|| {
|
||||
Icon::new(IconType::Check)
|
||||
.size(14.)
|
||||
.color(colors::brand())
|
||||
.into_element()
|
||||
}))
|
||||
};
|
||||
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.cross_align(Alignment::Center)
|
||||
.spacing(8.)
|
||||
.padding(Gaps::new_symmetric(5., 8.))
|
||||
.corner_radius(CornerRadius::new_all(6.))
|
||||
.background(bg)
|
||||
.content(Content::Flex)
|
||||
.a11y_id(a11y_id)
|
||||
.a11y_focusable(true)
|
||||
.a11y_role(AccessibilityRole::MenuItemRadio)
|
||||
.maybe(focused, |el| {
|
||||
el.border(crate::ui::border_all_color(1., colors::brand()))
|
||||
})
|
||||
.on_pointer_enter(|_| Cursor::set(CursorIcon::Pointer))
|
||||
.on_pointer_leave(|_| Cursor::set(CursorIcon::default()))
|
||||
.on_all_press(self.on_press.clone())
|
||||
.child(
|
||||
label()
|
||||
.text(text)
|
||||
.font_size(12.)
|
||||
.width(Size::flex(1.0))
|
||||
.color(color),
|
||||
)
|
||||
.maybe_child(selected.then(|| {
|
||||
Icon::new(IconType::Check)
|
||||
.size(14.)
|
||||
.color(colors::brand())
|
||||
.into_element()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn add_from_file_button(
|
||||
|
|
@ -470,14 +523,29 @@ impl Component for ContentBox {
|
|||
ContentKind::Other => empty_state(noun_plural).into_element(),
|
||||
});
|
||||
|
||||
let header = (count > 0)
|
||||
.then(|| match kind {
|
||||
ContentKind::External => Some(
|
||||
action_header(browse_button(cluster_id, package_type)).into_element(),
|
||||
),
|
||||
ContentKind::Local => Some(
|
||||
action_header(add_from_file_button(cluster_id, content_type, dispatch.clone()))
|
||||
.into_element(),
|
||||
),
|
||||
ContentKind::Other => None,
|
||||
})
|
||||
.flatten();
|
||||
|
||||
let mut bottom_corners = CornerRadius::new_all(0.);
|
||||
bottom_corners.fill_bottom(12.);
|
||||
|
||||
let drop_dispatch = dispatch.clone();
|
||||
|
||||
rect()
|
||||
.vertical()
|
||||
.width(Size::fill())
|
||||
.height(Size::flex(1.0))
|
||||
.spacing(8.)
|
||||
.padding(Gaps::new_all(8.))
|
||||
.corner_radius(bottom_corners)
|
||||
.background(colors::page_elevated())
|
||||
|
|
@ -498,11 +566,22 @@ impl Component for ContentBox {
|
|||
drop_dispatch.import_local_file(cluster_id, content_type, path.clone());
|
||||
}
|
||||
})
|
||||
.maybe_child(header)
|
||||
.maybe_child(scroll)
|
||||
.maybe_child(empty)
|
||||
}
|
||||
}
|
||||
|
||||
fn action_header(button: impl IntoElement) -> impl IntoElement {
|
||||
rect()
|
||||
.horizontal()
|
||||
.width(Size::fill())
|
||||
.cross_align(Alignment::Center)
|
||||
.content(Content::Flex)
|
||||
.child(rect().width(Size::flex(1.0)))
|
||||
.child(button)
|
||||
}
|
||||
|
||||
fn grid_content(
|
||||
items: &[PackageEntry],
|
||||
package_type: &'static str,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue