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::density::{dp, spacing};
22use teksilo_core::styles::{
23 FocusRingStroke, SegmentSlots, SegmentedControlStyle, SegmentedControlStyleConfig,
24};
25use teksilo_core::widget::{LayoutContext, LayoutResponse, PaintContext, Widget};
26use teksilo_core::widget_id::WidgetId;
27use teksilo_tokens::{CornerRadius, InputTokens, TargetRole};
28
29pub const SEGMENTED_CONTROL_HEIGHT: f32 = 24.0;
32pub const SEGMENTED_CONTROL_PADDING_HORIZONTAL: f32 = 12.0;
33pub const SEGMENTED_CONTROL_PADDING_VERTICAL: f32 = 6.0;
34pub const SEGMENTED_CONTROL_CORNER_RADIUS: f32 = 3.0;
35pub const SEGMENTED_CONTROL_BORDER_WIDTH: f32 = 1.0;
36
37#[derive(Debug, Clone, Copy, PartialEq)]
44pub struct SegmentedControlRecipe {
45 pub height: f32,
46 pub padding_horizontal: f32,
47 pub padding_vertical: f32,
48 pub corner_radius: f32,
49 pub border_width: f32,
50}
51
52impl SegmentedControlRecipe {
53 pub fn for_tokens(tokens: &InputTokens) -> Self {
59 Self {
60 height: dp(SEGMENTED_CONTROL_HEIGHT, TargetRole::Target, tokens),
61 padding_horizontal: spacing(SEGMENTED_CONTROL_PADDING_HORIZONTAL, tokens),
62 padding_vertical: spacing(SEGMENTED_CONTROL_PADDING_VERTICAL, tokens),
63 corner_radius: SEGMENTED_CONTROL_CORNER_RADIUS,
64 border_width: SEGMENTED_CONTROL_BORDER_WIDTH,
65 }
66 }
67}
68
69impl Default for SegmentedControlRecipe {
70 fn default() -> Self {
71 Self::for_tokens(&InputTokens::default())
72 }
73}
74
75#[derive(Debug, Default, Clone, Copy)]
77pub struct RecipeSegmentedControlStyle {
78 pub recipe: SegmentedControlRecipe,
79}
80
81impl RecipeSegmentedControlStyle {
82 pub fn new(recipe: SegmentedControlRecipe) -> Self {
83 Self { recipe }
84 }
85
86 pub fn for_tokens(tokens: &InputTokens) -> Self {
93 Self {
94 recipe: SegmentedControlRecipe::for_tokens(tokens),
95 }
96 }
97}
98
99impl SegmentedControlStyle for RecipeSegmentedControlStyle {
100 fn make_body(&self, cfg: &SegmentedControlStyleConfig, ctx: &mut BuildContext) -> WidgetId {
101 ctx.add(SegmentedControlChrome {
102 slots: cfg.slots.clone(),
103 selected: cfg.selected.clone(),
104 hovered_segment: cfg.hovered_segment.clone(),
105 focus_origin: cfg.focus_origin.clone(),
106 is_enabled: cfg.is_enabled.clone(),
107 recipe: self.recipe,
108 })
109 }
110}
111
112struct SegmentedControlChrome {
119 slots: SegmentSlots,
120 selected: Signal<usize>,
121 hovered_segment: Signal<Option<usize>>,
122 focus_origin: Signal<Option<FocusOrigin>>,
123 is_enabled: Signal<bool>,
125 recipe: SegmentedControlRecipe,
126}
127
128impl std::fmt::Debug for SegmentedControlChrome {
129 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130 f.debug_struct("SegmentedControlChrome")
131 .field("slots", &self.slots.len())
132 .finish()
133 }
134}
135
136impl Widget for SegmentedControlChrome {
137 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
138 let id = ctx.self_id();
139 let registry = ctx.binding_registry();
140 self.selected
146 .bind_to(id, registry, BindingLevel::RepaintOnly);
147 self.hovered_segment
148 .bind_to(id, registry, BindingLevel::RepaintOnly);
149 self.focus_origin
150 .bind_to(id, registry, BindingLevel::RepaintOnly);
151 self.is_enabled
155 .bind_to(id, registry, BindingLevel::RepaintOnly);
156 vec![]
157 }
158
159 fn layout_response(&self, proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
160 Size::new(
161 proposal.width.unwrap_or(0.0),
162 proposal.height.unwrap_or(0.0),
163 )
164 .into()
165 }
166
167 fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
168 let colors = &ctx.theme.colors;
169 let shape = &ctx.theme.shape;
170 let bw = self.recipe.border_width;
171 let frame_cr = CornerRadius::uniform(self.recipe.corner_radius);
172
173 let selected = self.selected.get();
174 let hovered = self.hovered_segment.get();
175 let focus_origin = self.focus_origin.get();
176 let focused = focus_origin.is_some();
177 let keyboard_focused = focus_origin == Some(FocusOrigin::Keyboard);
178 let is_enabled = self.is_enabled.get();
182
183 self.slots.with(|geometry| {
184 if geometry.segments.is_empty() {
185 return;
188 }
189
190 let frame_border = if !is_enabled {
192 colors.border
193 } else {
194 colors.border_strong
195 };
196 canvas.stroke_rounded_rect(geometry.frame, frame_cr, frame_border, bw);
197
198 let selected_slot = geometry.order.iter().position(|&s| s == selected);
202 let hovered_slot = hovered.and_then(|h| geometry.order.iter().position(|&s| s == h));
203
204 if is_enabled
207 && let Some(slot) = hovered_slot
208 && Some(slot) != selected_slot
209 && let Some(rect) = geometry.segments.get(slot)
210 {
211 canvas.fill_rounded_rect(*rect, frame_cr, colors.surface_hover);
212 }
213
214 if let Some(slot) = selected_slot
220 && let Some(base) = geometry.segments.get(slot)
221 {
222 let sel = Rect::new(
223 base.x - bw,
224 base.y - bw,
225 base.width + bw * 2.0,
226 base.height + bw * 2.0,
227 );
228 let (sel_bg, sel_border) = if !is_enabled {
229 (colors.surface_selected_inactive, colors.border)
230 } else if focused {
231 (colors.accent, colors.accent)
232 } else {
233 (colors.surface_selected_inactive, colors.border_strong)
234 };
235 canvas.fill_rounded_rect(sel, frame_cr, sel_bg);
236 canvas.stroke_rounded_rect(sel, frame_cr, sel_border, bw);
237 }
238
239 if let Some(overflow) = geometry.overflow {
242 canvas.fill_rect(
243 Rect::new(overflow.x, overflow.y, bw, overflow.height),
244 frame_border,
245 );
246 }
247 });
248
249 if keyboard_focused {
253 let envelope = shape.focus_ring_envelope();
257 let visual = bounds.inset(envelope, envelope, envelope, envelope);
258 let ring = FocusRingStroke::around(visual, self.recipe.corner_radius, shape);
259 canvas.stroke_rounded_rect(
260 ring.rect,
261 CornerRadius::uniform(ring.corner_radius),
262 colors.focus_ring,
263 shape.focus_ring_width,
264 );
265 }
266 }
267
268 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
269 builder.set_hidden();
273 }
274}