1use teksilo_canvas::{Canvas, Path, Point, Rect, Size, SizeProposal};
19use teksilo_core::accessibility::AccessNodeBuilder;
20use teksilo_core::binding::BindingLevel;
21use teksilo_core::build_context::BuildContext;
22use teksilo_core::focus::FocusOrigin;
23use teksilo_core::signal::Signal;
24use teksilo_core::styles::{CheckboxState, CheckboxStyle, CheckboxStyleConfig, CheckboxVariant};
25use teksilo_core::widget::{LayoutContext, LayoutResponse, PaintContext, Widget, WidgetPlacement};
26use teksilo_core::widget_id::WidgetId;
27use teksilo_tokens::{Color, CornerRadius};
28
29pub const CHECKBOX_BOX_VISUAL_SIZE: f32 = 19.0;
31pub const CHECKBOX_BOX_HIT_AREA: f32 = 24.0;
32pub const CHECKBOX_LABEL_GAP: f32 = 6.0;
33pub const CHECKBOX_CORNER_RADIUS: f32 = 3.0;
34
35#[derive(Debug, Clone, Copy, PartialEq)]
40pub struct CheckboxRecipe {
41 pub box_visual_size: f32,
42 pub box_hit_area: f32,
43 pub label_gap: f32,
44 pub corner_radius: f32,
45}
46
47impl Default for CheckboxRecipe {
48 fn default() -> Self {
49 Self {
50 box_visual_size: CHECKBOX_BOX_VISUAL_SIZE,
51 box_hit_area: CHECKBOX_BOX_HIT_AREA,
52 label_gap: CHECKBOX_LABEL_GAP,
53 corner_radius: CHECKBOX_CORNER_RADIUS,
54 }
55 }
56}
57
58#[derive(Debug, Default, Clone, Copy)]
62pub struct RecipeCheckboxStyle {
63 pub recipe: CheckboxRecipe,
64}
65
66impl RecipeCheckboxStyle {
67 pub fn new(recipe: CheckboxRecipe) -> Self {
68 Self { recipe }
69 }
70}
71
72impl CheckboxStyle for RecipeCheckboxStyle {
73 fn make_body(&self, cfg: &CheckboxStyleConfig, ctx: &mut BuildContext) -> WidgetId {
74 let focus_origin = cfg
75 .is_focused
76 .zip(&cfg.is_hovered)
77 .map(|(focused, hovered)| {
78 if *focused {
79 Some(if *hovered {
80 FocusOrigin::Pointer
81 } else {
82 FocusOrigin::Keyboard
83 })
84 } else {
85 None
86 }
87 });
88
89 ctx.add(CheckboxBody {
90 state: cfg.state.clone(),
91 is_hovered: cfg.is_hovered.clone(),
92 is_pressed: cfg.is_pressed.clone(),
93 is_disabled: cfg.is_disabled.clone(),
94 focus_origin,
95 variant: cfg.variant,
96 recipe: self.recipe,
97 })
98 }
99}
100
101struct CheckboxBody {
102 state: Signal<CheckboxState>,
103 is_hovered: Signal<bool>,
104 is_pressed: Signal<bool>,
105 is_disabled: Signal<bool>,
106 focus_origin: Signal<Option<FocusOrigin>>,
107 variant: CheckboxVariant,
108 recipe: CheckboxRecipe,
109}
110
111impl std::fmt::Debug for CheckboxBody {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 f.debug_struct("CheckboxBody")
114 .field("variant", &self.variant)
115 .finish()
116 }
117}
118
119impl Widget for CheckboxBody {
120 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
121 let id = ctx.self_id();
122 let registry = ctx.binding_registry();
123 self.state.bind_to(id, registry, BindingLevel::RepaintOnly);
124 self.is_hovered
125 .bind_to(id, registry, BindingLevel::RepaintOnly);
126 self.is_pressed
127 .bind_to(id, registry, BindingLevel::RepaintOnly);
128 self.is_disabled
129 .bind_to(id, registry, BindingLevel::RepaintOnly);
130 self.focus_origin
131 .bind_to(id, registry, BindingLevel::RepaintOnly);
132 vec![]
133 }
134
135 fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
136 Size::new(self.recipe.box_visual_size, self.recipe.box_visual_size).into()
137 }
138
139 fn place_children(
140 &self,
141 _bounds: Rect,
142 _proposal: SizeProposal,
143 _children: &mut [WidgetPlacement],
144 _ctx: &LayoutContext,
145 ) {
146 }
147
148 fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
149 let colors = &ctx.theme.colors;
150 let state = self.state.get();
151 let is_filled = matches!(state, CheckboxState::Checked | CheckboxState::Indeterminate);
152 let disabled = self.is_disabled.get();
153 let pressed = self.is_pressed.get();
154 let hovered = self.is_hovered.get();
155 let focused = self.focus_origin.get().is_some();
156
157 let bg = if disabled {
160 if is_filled {
161 colors.accent_disabled
162 } else {
163 Color::TRANSPARENT
164 }
165 } else if is_filled {
166 if pressed {
167 colors.accent_pressed
168 } else if hovered {
169 colors.accent_hover
170 } else {
171 colors.accent
172 }
173 } else {
174 Color::TRANSPARENT
175 };
176
177 let border_color = if focused {
180 colors.border_focused
181 } else if disabled {
182 colors.accent_disabled
183 } else if is_filled {
184 Color::TRANSPARENT
185 } else if hovered {
186 colors.border_strong
187 } else {
188 colors.border
189 };
190
191 let border_width = if focused {
192 ctx.theme.shape.focus_ring_width
193 } else {
194 ctx.theme.shape.border_width
195 };
196
197 let corner = match self.variant {
201 CheckboxVariant::Square => CornerRadius::uniform(self.recipe.corner_radius),
202 CheckboxVariant::Rounded => CornerRadius::uniform(self.recipe.corner_radius * 2.0),
203 CheckboxVariant::Circle => CornerRadius::uniform(self.recipe.box_visual_size / 2.0),
204 };
205
206 canvas.fill_rounded_rect(bounds, corner, bg);
207 if border_width > 0.0 && border_color != Color::TRANSPARENT {
208 canvas.stroke_rounded_rect(bounds, corner, border_color, border_width);
209 }
210
211 if matches!(state, CheckboxState::Unchecked) {
214 return;
215 }
216 let glyph_color = if disabled {
217 colors.text_disabled
218 } else {
219 colors.text_on_accent
220 };
221 let glyph_size = self.recipe.box_visual_size * 0.75;
222 let glyph_rect = Rect::new(
223 bounds.x + (bounds.width - glyph_size) / 2.0,
224 bounds.y + (bounds.height - glyph_size) / 2.0,
225 glyph_size,
226 glyph_size,
227 );
228 let stroke_w = (glyph_size * 0.18).max(1.5);
229
230 let mut path = Path::new();
231 match state {
232 CheckboxState::Checked => {
233 path.move_to(Point::new(
235 glyph_rect.x + glyph_size * 0.20,
236 glyph_rect.y + glyph_size * 0.55,
237 ));
238 path.line_to(Point::new(
239 glyph_rect.x + glyph_size * 0.43,
240 glyph_rect.y + glyph_size * 0.78,
241 ));
242 path.line_to(Point::new(
243 glyph_rect.x + glyph_size * 0.80,
244 glyph_rect.y + glyph_size * 0.28,
245 ));
246 }
247 CheckboxState::Indeterminate => {
248 path.move_to(Point::new(
250 glyph_rect.x + glyph_size * 0.20,
251 glyph_rect.y + glyph_size * 0.50,
252 ));
253 path.line_to(Point::new(
254 glyph_rect.x + glyph_size * 0.80,
255 glyph_rect.y + glyph_size * 0.50,
256 ));
257 }
258 CheckboxState::Unchecked => unreachable!(),
259 }
260 canvas.stroke_path(&path, glyph_color, stroke_w);
261 }
262
263 fn accessibility(&self, _builder: &mut AccessNodeBuilder) {
264 }
267}