teksilo_widgets/color_picker/
swatch_grid.rs1use std::rc::Rc;
15
16use teksilo_canvas::{Rect, SizeProposal};
17use teksilo_core::accessibility::AccessNodeBuilder;
18use teksilo_core::accesskit::Role;
19use teksilo_core::build_context::BuildContext;
20use teksilo_core::event::{EventResponse, Key, WidgetEvent};
21use teksilo_core::signal::Signal;
22use teksilo_core::widget::{EventContext, LayoutContext, LayoutResponse, Widget, WidgetPlacement};
23use teksilo_core::widget_builder::HandlerSet;
24use teksilo_core::widget_id::WidgetId;
25use teksilo_i18n::resolve_message_widget;
26use teksilo_tokens::Color;
27
28use super::swatch::ColorSwatch;
29use crate::primitives::{Grid, TrackSize};
30
31pub(crate) struct SwatchGrid {
32 swatches: Signal<Vec<Color>>,
33 selected: Signal<Color>,
34 columns: usize,
35 on_select: Rc<dyn Fn(Color, &mut EventContext)>,
36 focused_index: Signal<usize>,
40 initial_enabled: bool,
42 root_child_id: Option<WidgetId>,
43}
44
45impl SwatchGrid {
46 pub(crate) fn new(
47 swatches: Signal<Vec<Color>>,
48 selected: Signal<Color>,
49 columns: usize,
50 on_select: Rc<dyn Fn(Color, &mut EventContext)>,
51 ) -> Self {
52 Self {
53 swatches,
54 selected,
55 columns: columns.max(1),
56 on_select,
57 focused_index: Signal::new(0),
58 initial_enabled: true,
59 root_child_id: None,
60 }
61 }
62
63 pub(crate) fn enabled(mut self, enabled: bool) -> Self {
65 self.initial_enabled = enabled;
66 self
67 }
68}
69
70impl std::fmt::Debug for SwatchGrid {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 f.debug_struct("SwatchGrid")
73 .field("columns", &self.columns)
74 .finish_non_exhaustive()
75 }
76}
77
78impl Widget for SwatchGrid {
79 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
80 let self_id = ctx.self_id();
81 if !self.initial_enabled {
83 ctx.enabled_when(self_id, false);
84 }
85 let registry = ctx.binding_registry();
86 self.swatches.bind_to(
89 self_id,
90 registry,
91 teksilo_core::binding::BindingLevel::Relayout,
92 );
93 self.selected.bind_to(
94 self_id,
95 registry,
96 teksilo_core::binding::BindingLevel::RepaintOnly,
97 );
98
99 let swatches = self.swatches.get();
100 let columns = self.columns;
101 use crate::styles::recipe_color_picker_style as cp;
102
103 let on_select = self.on_select.clone();
105 let selected = self.selected.clone();
106 let mut grid = Grid::new()
107 .columns(vec![TrackSize::Auto; columns])
108 .row_gap(cp::SWATCH_SPACING)
109 .column_gap(cp::SWATCH_SPACING);
110 for color in &swatches {
111 let color = *color;
112 let on_select = on_select.clone();
113 let is_selected = selected.get() == color;
114 let cell = ColorSwatch::new(color)
115 .selected(is_selected)
116 .enabled(self.initial_enabled)
117 .on_activate_fn(move |ctx_evt| {
118 (on_select)(color, ctx_evt);
119 });
120 grid = grid.child(cell);
121 }
122 let root = ctx.add(grid);
123 self.root_child_id = Some(root);
124
125 let count = swatches.len();
128 let columns_for_keys = self.columns;
129 let focused_index = self.focused_index.clone();
130 let on_select_keys = self.on_select.clone();
131 let swatches_for_keys = self.swatches.clone();
132 let handlers = HandlerSet::new()
133 .focusable(count > 0)
137 .on_key(move |event, ctx_evt| {
138 if count == 0 {
139 return EventResponse::Ignored;
140 }
141 let WidgetEvent::KeyDown { key, modifiers, .. } = event else {
142 return EventResponse::Ignored;
143 };
144 let mut idx = focused_index.get().min(count.saturating_sub(1));
145 let last = count.saturating_sub(1);
146 let row = idx / columns_for_keys;
147 let col = idx % columns_for_keys;
148 let row_start = row * columns_for_keys;
149 let row_end = ((row + 1) * columns_for_keys - 1).min(last);
150 match key {
151 Key::ArrowLeft => {
152 idx = idx.saturating_sub(1);
153 }
154 Key::ArrowRight => {
155 if idx < last {
156 idx += 1;
157 }
158 }
159 Key::ArrowUp => {
160 if idx >= columns_for_keys {
161 idx -= columns_for_keys;
162 }
163 }
164 Key::ArrowDown => {
165 if idx + columns_for_keys <= last {
166 idx += columns_for_keys;
167 }
168 }
169 Key::Home => {
172 idx = if modifiers.command() { 0 } else { row_start };
173 }
174 Key::End => {
175 idx = if modifiers.command() { last } else { row_end };
176 }
177 Key::Enter | Key::Space => {
178 let list = swatches_for_keys.get();
179 if let Some(c) = list.get(idx) {
180 (on_select_keys)(*c, ctx_evt);
181 }
182 return EventResponse::Handled;
183 }
184 _ => return EventResponse::Ignored,
185 }
186 let _ = (row, col);
187 focused_index.set(idx);
188 EventResponse::Handled
189 });
190 ctx.apply_self_handlers(handlers);
191
192 vec![root]
193 }
194
195 fn layout_response(&self, proposal: SizeProposal, ctx: &LayoutContext) -> LayoutResponse {
196 match self.root_child_id {
197 Some(id) => ctx
198 .child_layout_response(id, proposal)
199 .unwrap_or_else(|| proposal.resolve(0.0, 0.0).into()),
200 None => proposal.resolve(0.0, 0.0).into(),
201 }
202 }
203
204 fn place_children(
205 &self,
206 bounds: Rect,
207 _proposal: SizeProposal,
208 children: &mut [WidgetPlacement],
209 _ctx: &LayoutContext,
210 ) {
211 for child in children.iter_mut() {
212 child.origin = bounds.origin();
213 child.size = bounds.size();
214 }
215 }
216
217 fn children(&self) -> Vec<WidgetId> {
218 self.root_child_id.into_iter().collect()
219 }
220
221 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
222 builder.set_role(Role::Grid);
223 builder.set_name(resolve_message_widget("color-picker-swatches-name", &[]));
224 }
226}