1use teksilo_canvas::{Canvas, Rect, Size, SizeProposal};
16use teksilo_core::accessibility::AccessNodeBuilder;
17use teksilo_core::binding::BindingLevel;
18use teksilo_core::build_context::BuildContext;
19use teksilo_core::focus::FocusOrigin;
20use teksilo_core::signal::Signal;
21use teksilo_core::styles::{SegmentSlots, SegmentedControlStyle, SegmentedControlStyleConfig};
22use teksilo_core::widget::{LayoutContext, LayoutResponse, PaintContext, Widget};
23use teksilo_core::widget_id::WidgetId;
24use teksilo_tokens::CornerRadius;
25
26pub const SEGMENTED_CONTROL_HEIGHT: f32 = 24.0;
29pub const SEGMENTED_CONTROL_PADDING_HORIZONTAL: f32 = 12.0;
30pub const SEGMENTED_CONTROL_PADDING_VERTICAL: f32 = 6.0;
31pub const SEGMENTED_CONTROL_CORNER_RADIUS: f32 = 3.0;
32pub const SEGMENTED_CONTROL_BORDER_WIDTH: f32 = 1.0;
33
34#[derive(Debug, Clone, Copy, PartialEq)]
41pub struct SegmentedControlRecipe {
42 pub height: f32,
43 pub padding_horizontal: f32,
44 pub padding_vertical: f32,
45 pub corner_radius: f32,
46 pub border_width: f32,
47}
48
49impl Default for SegmentedControlRecipe {
50 fn default() -> Self {
51 Self {
52 height: SEGMENTED_CONTROL_HEIGHT,
53 padding_horizontal: SEGMENTED_CONTROL_PADDING_HORIZONTAL,
54 padding_vertical: SEGMENTED_CONTROL_PADDING_VERTICAL,
55 corner_radius: SEGMENTED_CONTROL_CORNER_RADIUS,
56 border_width: SEGMENTED_CONTROL_BORDER_WIDTH,
57 }
58 }
59}
60
61#[derive(Debug, Default, Clone, Copy)]
63pub struct RecipeSegmentedControlStyle {
64 pub recipe: SegmentedControlRecipe,
65}
66
67impl RecipeSegmentedControlStyle {
68 pub fn new(recipe: SegmentedControlRecipe) -> Self {
69 Self { recipe }
70 }
71}
72
73impl SegmentedControlStyle for RecipeSegmentedControlStyle {
74 fn make_body(&self, cfg: &SegmentedControlStyleConfig, ctx: &mut BuildContext) -> WidgetId {
75 ctx.add(SegmentedControlChrome {
76 slots: cfg.slots.clone(),
77 selected: cfg.selected.clone(),
78 hovered_segment: cfg.hovered_segment.clone(),
79 focus_origin: cfg.focus_origin.clone(),
80 is_enabled: cfg.is_enabled.clone(),
81 recipe: self.recipe,
82 })
83 }
84}
85
86struct SegmentedControlChrome {
93 slots: SegmentSlots,
94 selected: Signal<usize>,
95 hovered_segment: Signal<Option<usize>>,
96 focus_origin: Signal<Option<FocusOrigin>>,
97 is_enabled: Signal<bool>,
99 recipe: SegmentedControlRecipe,
100}
101
102impl std::fmt::Debug for SegmentedControlChrome {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 f.debug_struct("SegmentedControlChrome")
105 .field("slots", &self.slots.len())
106 .finish()
107 }
108}
109
110impl Widget for SegmentedControlChrome {
111 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
112 let id = ctx.self_id();
113 let registry = ctx.binding_registry();
114 self.selected
120 .bind_to(id, registry, BindingLevel::RepaintOnly);
121 self.hovered_segment
122 .bind_to(id, registry, BindingLevel::RepaintOnly);
123 self.focus_origin
124 .bind_to(id, registry, BindingLevel::RepaintOnly);
125 self.is_enabled
129 .bind_to(id, registry, BindingLevel::RepaintOnly);
130 vec![]
131 }
132
133 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
134 Size::new(
135 proposal.width.unwrap_or(0.0),
136 proposal.height.unwrap_or(0.0),
137 )
138 .into()
139 }
140
141 fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
142 let colors = &ctx.theme.colors;
143 let shape = &ctx.theme.shape;
144 let bw = self.recipe.border_width;
145 let frame_cr = CornerRadius::uniform(self.recipe.corner_radius);
146
147 let selected = self.selected.get();
148 let hovered = self.hovered_segment.get();
149 let focus_origin = self.focus_origin.get();
150 let focused = focus_origin.is_some();
151 let keyboard_focused = focus_origin == Some(FocusOrigin::Keyboard);
152 let is_enabled = self.is_enabled.get();
156
157 self.slots.with(|geometry| {
158 if geometry.segments.is_empty() {
159 return;
162 }
163
164 let frame_border = if !is_enabled {
166 colors.border
167 } else {
168 colors.border_strong
169 };
170 canvas.stroke_rounded_rect(geometry.frame, frame_cr, frame_border, bw);
171
172 let selected_slot = geometry.order.iter().position(|&s| s == selected);
176 let hovered_slot = hovered.and_then(|h| geometry.order.iter().position(|&s| s == h));
177
178 if is_enabled
181 && let Some(slot) = hovered_slot
182 && Some(slot) != selected_slot
183 && let Some(rect) = geometry.segments.get(slot)
184 {
185 canvas.fill_rounded_rect(*rect, frame_cr, colors.surface_hover);
186 }
187
188 if let Some(slot) = selected_slot
194 && let Some(base) = geometry.segments.get(slot)
195 {
196 let sel = Rect::new(
197 base.x - bw,
198 base.y - bw,
199 base.width + bw * 2.0,
200 base.height + bw * 2.0,
201 );
202 let (sel_bg, sel_border) = if !is_enabled {
203 (colors.surface_selected_inactive, colors.border)
204 } else if focused {
205 (colors.accent, colors.accent)
206 } else {
207 (colors.surface_selected_inactive, colors.border_strong)
208 };
209 canvas.fill_rounded_rect(sel, frame_cr, sel_bg);
210 canvas.stroke_rounded_rect(sel, frame_cr, sel_border, bw);
211 }
212
213 if let Some(overflow) = geometry.overflow {
216 canvas.fill_rect(
217 Rect::new(overflow.x, overflow.y, bw, overflow.height),
218 frame_border,
219 );
220 }
221 });
222
223 if keyboard_focused {
227 let half_stroke = shape.focus_ring_width * 0.5;
228 let ring_rect = Rect::new(
229 bounds.x + half_stroke,
230 bounds.y + half_stroke,
231 (bounds.width - half_stroke * 2.0).max(0.0),
232 (bounds.height - half_stroke * 2.0).max(0.0),
233 );
234 let ring_radius = self.recipe.corner_radius + shape.focus_ring_offset + half_stroke;
235 canvas.stroke_rounded_rect(
236 ring_rect,
237 CornerRadius::uniform(ring_radius),
238 colors.focus_ring,
239 shape.focus_ring_width,
240 );
241 }
242 }
243
244 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
245 builder.set_hidden();
249 }
250}