teksilo_widgets/color_picker/
swatch.rs1use std::cell::Cell;
30use std::rc::Rc;
31
32use teksilo_canvas::{Canvas, Rect, Size, SizeProposal};
33use teksilo_core::accessibility::AccessNodeBuilder;
34use teksilo_core::accesskit::{Action, Role};
35use teksilo_core::build_context::BuildContext;
36use teksilo_core::event::{EventResponse, Key, WidgetEvent};
37use teksilo_core::focus::FocusOrigin;
38use teksilo_core::widget::{
39 CursorIcon, EventContext, LayoutContext, LayoutResponse, PaintContext, Widget, WidgetPlacement,
40};
41use teksilo_core::widget_builder::HandlerSet;
42use teksilo_core::widget_id::WidgetId;
43use teksilo_i18n::{LocalizedString, resolve_message_widget};
44use teksilo_tokens::{Color, CornerRadius};
45
46use super::alpha_strip::paint_checkerboard;
47
48type ActivateFn = Rc<dyn Fn(&mut EventContext)>;
49
50pub struct ColorSwatch {
58 color: teksilo_core::signal::Prop<Color>,
59 selected: bool,
60 label: Option<LocalizedString>,
61 size: Option<f32>,
62 corner_radius: Option<f32>,
63 enabled: teksilo_core::signal::Prop<bool>,
66 on_activate: Option<ActivateFn>,
67 focus_origin: Rc<Cell<Option<FocusOrigin>>>,
68 tooltip_text: Option<LocalizedString>,
72 rich_tooltip_source: Option<crate::tooltip::RichTooltipSource>,
74 composite_tooltip_content: Option<Box<dyn Widget>>,
76}
77
78impl ColorSwatch {
79 pub fn new(color: impl Into<teksilo_core::signal::Prop<Color>>) -> Self {
83 Self {
84 color: color.into(),
85 selected: false,
86 label: None,
87 size: None,
88 corner_radius: None,
89 enabled: teksilo_core::signal::Prop::Static(true),
90 on_activate: None,
91 focus_origin: Rc::new(Cell::new(None)),
92 tooltip_text: None,
93 rich_tooltip_source: None,
94 composite_tooltip_content: None,
95 }
96 }
97
98 pub fn selected(mut self, selected: bool) -> Self {
101 self.selected = selected;
102 self
103 }
104
105 pub fn label(mut self, label: impl Into<LocalizedString>) -> Self {
108 self.label = Some(label.into());
109 self
110 }
111
112 pub fn size(mut self, size: f32) -> Self {
115 self.size = Some(size.max(0.0));
116 self
117 }
118
119 pub fn corner_radius(mut self, r: f32) -> Self {
122 self.corner_radius = Some(r.max(0.0));
123 self
124 }
125
126 pub fn enabled(mut self, enabled: impl Into<teksilo_core::signal::Prop<bool>>) -> Self {
129 self.enabled = enabled.into();
130 self
131 }
132
133 pub fn on_activate_fn(mut self, f: impl Fn(&mut EventContext) + 'static) -> Self {
136 self.on_activate = Some(Rc::new(f));
137 self
138 }
139
140 pub fn tooltip(mut self, text: impl Into<LocalizedString>) -> Self {
145 self.tooltip_text = Some(text.into());
146 self.rich_tooltip_source = None;
147 self.composite_tooltip_content = None;
148 self
149 }
150
151 pub fn rich_tooltip(mut self, key: impl Into<String>) -> Self {
156 self.rich_tooltip_source = Some(crate::tooltip::RichTooltipSource::Key(key.into()));
157 self.tooltip_text = None;
158 self.composite_tooltip_content = None;
159 self
160 }
161
162 pub fn rich_tooltip_content(mut self, content: crate::tooltip::TooltipContent) -> Self {
167 self.rich_tooltip_source = Some(crate::tooltip::RichTooltipSource::Content(content));
168 self.tooltip_text = None;
169 self.composite_tooltip_content = None;
170 self
171 }
172
173 pub fn composite_tooltip(mut self, content: impl Widget + 'static) -> Self {
178 self.composite_tooltip_content = Some(Box::new(content));
179 self.tooltip_text = None;
180 self.rich_tooltip_source = None;
181 self
182 }
183}
184
185impl std::fmt::Debug for ColorSwatch {
186 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187 f.debug_struct("ColorSwatch")
188 .field("color", &self.color.get())
189 .field("selected", &self.selected)
190 .field("enabled", &self.enabled.get())
191 .finish_non_exhaustive()
192 }
193}
194
195impl Widget for ColorSwatch {
196 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
197 let self_id = ctx.self_id();
198 ctx.enabled_when(self_id, self.enabled.clone());
200 let on_activate = self.on_activate.clone();
201 let mut handlers = HandlerSet::new()
204 .focusable(true)
205 .cursor(CursorIcon::Pointer);
206
207 if let Some(cb) = on_activate.clone() {
208 handlers = handlers.on_tap(move |_pos, ctx_evt| {
209 cb(ctx_evt);
210 });
211 }
212 if let Some(cb) = on_activate.clone() {
213 handlers = handlers.on_key(move |event, ctx_evt| {
214 let WidgetEvent::KeyDown { key, .. } = event else {
215 return EventResponse::Ignored;
216 };
217 match key {
218 Key::Enter | Key::Space => {
219 cb(ctx_evt);
220 EventResponse::Handled
221 }
222 _ => EventResponse::Ignored,
223 }
224 });
225 }
226 if let Some(cb) = on_activate {
227 handlers = handlers.on_access_action(move |action, ctx_evt| match action {
228 Action::Click => {
229 cb(ctx_evt);
230 EventResponse::Handled
231 }
232 _ => EventResponse::Ignored,
233 });
234 }
235
236 {
237 let focus_origin = self.focus_origin.clone();
238 handlers = handlers.on_focus(move |gained, _ctx| {
239 focus_origin.set(if gained {
240 Some(FocusOrigin::Keyboard)
241 } else {
242 None
243 });
244 });
245 }
246
247 ctx.apply_self_handlers(handlers);
248
249 if let Some(content) = self.composite_tooltip_content.take() {
251 let delay = ctx.theme().motion.tooltip_delay_heavy;
252 crate::tooltip::attach_composite_tooltip_boxed(ctx, self_id, content, delay);
253 } else if let Some(source) = self.rich_tooltip_source.clone() {
254 let delay = ctx.theme().motion.tooltip_delay;
255 crate::tooltip::attach_rich_tooltip_source(ctx, self_id, source, delay);
256 } else if let Some(text) = self.tooltip_text.clone() {
257 let delay = ctx.theme().motion.tooltip_delay;
258 crate::tooltip::attach_plain_tooltip(ctx, self_id, text, delay);
259 }
260
261 let self_id = ctx.self_id();
265 let registry = ctx.binding_registry();
266 self.color.register_if_bound(
267 self_id,
268 registry,
269 teksilo_core::binding::BindingLevel::AccessibilityOnly,
270 );
271 self.color.register_if_bound(
272 self_id,
273 registry,
274 teksilo_core::binding::BindingLevel::RepaintOnly,
275 );
276
277 Vec::new()
278 }
279
280 fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
281 use crate::styles::recipe_color_picker_style as cp;
282 let size = self.size.unwrap_or(cp::SWATCH_SIZE);
283 Size::new(size, size).into()
284 }
285
286 fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
287 use crate::styles::recipe_color_picker_style as cp;
288 let radius = CornerRadius::uniform(self.corner_radius.unwrap_or(cp::SWATCH_CORNER_RADIUS));
289 let color = self.color.get();
290
291 if color.a() < 1.0 {
293 paint_checkerboard(
294 canvas,
295 bounds,
296 cp::CHECKER_CELL,
297 cp::CHECKER_COLOR_A,
298 cp::CHECKER_COLOR_B,
299 );
300 }
301
302 canvas.fill_rounded_rect(bounds, radius, color);
303
304 if self.selected {
306 canvas.stroke_rounded_rect(
307 bounds,
308 radius,
309 ctx.theme.colors.accent,
310 cp::SWATCH_SELECTED_STROKE_WIDTH,
311 );
312 } else {
313 canvas.stroke_rounded_rect(bounds, radius, ctx.theme.colors.border, 1.0);
316 }
317
318 if self.focus_origin.get() == Some(FocusOrigin::Keyboard) {
320 let offset = ctx.theme.shape.focus_ring_offset;
321 let half = ctx.theme.shape.focus_ring_width * 0.5;
322 let inset = offset + half;
323 let ring = Rect::new(
324 bounds.x - inset,
325 bounds.y - inset,
326 bounds.width + inset * 2.0,
327 bounds.height + inset * 2.0,
328 );
329 canvas.stroke_rounded_rect(
330 ring,
331 CornerRadius::uniform(
332 self.corner_radius.unwrap_or(cp::SWATCH_CORNER_RADIUS) + inset,
333 ),
334 ctx.theme.colors.focus_ring,
335 ctx.theme.shape.focus_ring_width,
336 );
337 }
338 }
339
340 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
341 builder.set_role(Role::ColorWell);
342 let color = self.color.get();
343 builder.set_color_value(color);
344 let hex = color.to_hex_upper(color.a() < 1.0);
345 let name = match &self.label {
346 Some(ls) => ls.resolve_now(),
347 None => {
348 resolve_message_widget("color-picker-swatch-label", &[("hex", hex.clone().into())])
349 }
350 };
351 let display = if self.selected {
352 let suffix = resolve_message_widget("color-picker-swatch-selected-suffix", &[]);
353 format!("{}{}", name, suffix)
354 } else {
355 name
356 };
357 builder.set_name(display);
358 builder.set_value(hex);
359 if self.selected {
360 builder.set_selected(true);
361 }
362 builder.add_action(Action::Click);
364 builder.add_action(Action::Focus);
365 }
366
367 fn place_children(
368 &self,
369 _bounds: Rect,
370 _proposal: SizeProposal,
371 _children: &mut [WidgetPlacement],
372 _ctx: &LayoutContext,
373 ) {
374 }
375}