llimphi_widget_segmented/
lib.rs1#![forbid(unsafe_code)]
11
12use llimphi_ui::llimphi_layout::taffy::{
13 prelude::{length, percent, FlexDirection, Size, Style},
14 AlignItems, JustifyContent, Rect,
15};
16use llimphi_ui::llimphi_raster::peniko::Color;
17use llimphi_ui::llimphi_text::Alignment;
18use llimphi_ui::View;
19use llimphi_theme::{radius, Theme};
20
21#[derive(Debug, Clone, Copy)]
23pub struct SegmentedPalette {
24 pub bg_track: Color,
25 pub bg_active: Color,
26 pub fg_active: Color,
27 pub fg_inactive: Color,
28 pub fg_hover: Color,
29}
30
31impl SegmentedPalette {
32 pub fn from_theme(t: &Theme) -> Self {
33 Self {
34 bg_track: t.bg_button,
35 bg_active: t.bg_panel,
36 fg_active: t.fg_text,
37 fg_inactive: t.fg_muted,
38 fg_hover: t.fg_text,
39 }
40 }
41}
42
43pub fn segmented_view<Msg, F>(
46 labels: &[&str],
47 selected: usize,
48 make_msg: F,
49 palette: &SegmentedPalette,
50) -> View<Msg>
51where
52 Msg: Clone + 'static,
53 F: Fn(usize) -> Msg,
54{
55 let children: Vec<View<Msg>> = labels
56 .iter()
57 .enumerate()
58 .map(|(i, label)| segment_view(i, label, i == selected, make_msg(i), palette))
59 .collect();
60
61 View::new(Style {
62 flex_direction: FlexDirection::Row,
63 size: Size {
64 width: percent(1.0_f32),
65 height: length(28.0_f32),
66 },
67 padding: Rect {
68 left: length(2.0_f32),
69 right: length(2.0_f32),
70 top: length(2.0_f32),
71 bottom: length(2.0_f32),
72 },
73 gap: Size {
74 width: length(2.0_f32),
75 height: length(0.0_f32),
76 },
77 ..Default::default()
78 })
79 .fill(palette.bg_track)
80 .radius(radius::SM)
81 .children(children)
82}
83
84fn segment_view<Msg: Clone + 'static>(
85 _idx: usize,
86 label: &str,
87 is_active: bool,
88 msg: Msg,
89 palette: &SegmentedPalette,
90) -> View<Msg> {
91 let (bg, fg) = if is_active {
92 (Some(palette.bg_active), palette.fg_active)
93 } else {
94 (None, palette.fg_inactive)
95 };
96
97 let seg_radius = radius::XS;
98 let mut node = View::new(Style {
99 size: Size {
100 width: percent(1.0_f32),
101 height: percent(1.0_f32),
102 },
103 flex_grow: 1.0,
104 align_items: Some(AlignItems::Center),
105 justify_content: Some(JustifyContent::Center),
106 padding: Rect {
107 left: length(8.0_f32),
108 right: length(8.0_f32),
109 top: length(0.0_f32),
110 bottom: length(0.0_f32),
111 },
112 ..Default::default()
113 })
114 .radius(seg_radius)
115 .text_aligned(label.to_string(), 11.5, fg, Alignment::Center)
116 .role(llimphi_ui::Role::Tab)
119 .aria_label(label.to_string())
120 .aria_pressed(is_active)
121 .on_click(msg);
122
123 if let Some(c) = bg {
124 node = node.fill(c).paint_with(move |scene, _ts, rect| {
125 use llimphi_ui::llimphi_raster::kurbo::{Affine, Point, RoundedRect};
130 use llimphi_ui::llimphi_raster::peniko::{Fill, Gradient};
131 if rect.w <= 0.0 || rect.h <= 0.0 {
132 return;
133 }
134 let x0 = rect.x as f64;
135 let y0 = rect.y as f64;
136 let x1 = (rect.x + rect.w) as f64;
137 let y1 = (rect.y + rect.h) as f64;
138 let y_mid = y0 + (y1 - y0) * 0.5;
139 let rr = RoundedRect::new(x0, y0, x1, y1, seg_radius);
140 let top = Color::from_rgba8(255, 255, 255, 28);
141 let bot = Color::from_rgba8(255, 255, 255, 0);
142 let g = Gradient::new_linear(Point::new(x0, y0), Point::new(x0, y_mid))
143 .with_stops([top, bot].as_slice());
144 scene.fill(Fill::NonZero, Affine::IDENTITY, &g, None, &rr);
145 });
146 }
147 node
148}