teksilo_widgets/color_picker/
alpha_strip.rs1use std::cell::Cell;
19use std::rc::Rc;
20
21use teksilo_canvas::paint::GradientStop;
22use teksilo_canvas::{Canvas, Paint, Point, Rect, Size, SizeProposal};
23use teksilo_core::accessibility::AccessNodeBuilder;
24use teksilo_core::accesskit::{Action, Role};
25use teksilo_core::build_context::BuildContext;
26use teksilo_core::event::{EventResponse, Key, PointerButton, WidgetEvent};
27use teksilo_core::focus::FocusOrigin;
28use teksilo_core::gesture::DragPhase;
29use teksilo_core::signal::Signal;
30use teksilo_core::widget::{
31 CursorIcon, LayoutContext, LayoutResponse, PaintContext, Widget, WidgetPlacement,
32};
33use teksilo_core::widget_builder::HandlerSet;
34use teksilo_core::widget_id::WidgetId;
35use teksilo_tokens::{Color, CornerRadius, Orientation};
36
37pub(crate) struct AlphaStrip {
38 current_color: Signal<Color>,
40 alpha: Signal<f32>,
41 set_alpha: Rc<dyn Fn(f32)>,
42 dragging: Rc<Cell<bool>>,
43 cached_bounds: Rc<Cell<Rect>>,
44 focus_origin: Rc<Cell<Option<FocusOrigin>>>,
45 orientation: Orientation,
46 initial_enabled: bool,
48 label: String,
49}
50
51impl AlphaStrip {
52 pub(crate) fn new(
53 current_color: Signal<Color>,
54 alpha: Signal<f32>,
55 set_alpha: Rc<dyn Fn(f32)>,
56 dragging: Rc<Cell<bool>>,
57 ) -> Self {
58 Self {
59 current_color,
60 alpha,
61 set_alpha,
62 dragging,
63 cached_bounds: Rc::new(Cell::new(Rect::ZERO)),
64 focus_origin: Rc::new(Cell::new(None)),
65 orientation: Orientation::Vertical,
66 initial_enabled: true,
67 label: String::new(),
68 }
69 }
70
71 pub(crate) fn orientation(mut self, orientation: Orientation) -> Self {
72 self.orientation = orientation;
73 self
74 }
75
76 pub(crate) fn enabled(mut self, enabled: bool) -> Self {
78 self.initial_enabled = enabled;
79 self
80 }
81
82 pub(crate) fn label(mut self, label: impl Into<String>) -> Self {
83 self.label = label.into();
84 self
85 }
86}
87
88impl std::fmt::Debug for AlphaStrip {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 f.debug_struct("AlphaStrip").finish_non_exhaustive()
91 }
92}
93
94impl Widget for AlphaStrip {
95 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
96 let self_id = ctx.self_id();
97 if !self.initial_enabled {
99 ctx.enabled_when(self_id, false);
100 }
101 let registry = ctx.binding_registry();
102 self.current_color.bind_to(
104 self_id,
105 registry,
106 teksilo_core::binding::BindingLevel::RepaintOnly,
107 );
108 self.alpha.bind_to(
109 self_id,
110 registry,
111 teksilo_core::binding::BindingLevel::RepaintOnly,
112 );
113
114 let cached_bounds = self.cached_bounds.clone();
117 let dragging = self.dragging.clone();
118 let set_alpha = self.set_alpha.clone();
119 let orientation = self.orientation;
120
121 let apply: Rc<dyn Fn(f32, f32)> = {
122 let set_alpha = set_alpha.clone();
123 Rc::new(move |x: f32, y: f32| {
124 let bounds = cached_bounds.get();
125 let t = match orientation {
128 Orientation::Vertical => {
129 if bounds.height <= 0.0 {
130 return;
131 }
132 (y / bounds.height).clamp(0.0, 1.0)
133 }
134 Orientation::Horizontal => {
135 if bounds.width <= 0.0 {
136 return;
137 }
138 (x / bounds.width).clamp(0.0, 1.0)
139 }
140 };
141 (set_alpha)(t.clamp(0.0, 1.0));
143 })
144 };
145
146 let mut handlers = HandlerSet::new()
147 .focusable(true)
148 .cursor(CursorIcon::Pointer);
149
150 {
151 let dragging = dragging.clone();
152 let apply = apply.clone();
153 handlers = handlers.on_drag(move |phase, _ctx| match phase {
154 DragPhase::Started {
155 position,
156 button: PointerButton::Primary,
157 } => {
158 dragging.set(true);
159 apply(position.x, position.y);
160 }
161 DragPhase::Moved { position, .. } if dragging.get() => {
162 apply(position.x, position.y);
163 }
164 DragPhase::Ended { .. } => {
165 dragging.set(false);
166 }
167 _ => {}
168 });
169 }
170 {
171 let apply = apply.clone();
172 handlers = handlers.on_tap(move |event, _ctx| {
173 apply(event.position.x, event.position.y);
174 });
175 }
176
177 {
179 let set_alpha = set_alpha.clone();
180 let alpha = self.alpha.clone();
181 handlers = handlers.on_key(move |event, _ctx| {
182 let WidgetEvent::KeyDown { key, .. } = event else {
183 return EventResponse::Ignored;
184 };
185 match key {
186 Key::ArrowUp | Key::ArrowRight => {
187 (set_alpha)((alpha.get() + 0.01).clamp(0.0, 1.0));
188 EventResponse::Handled
189 }
190 Key::ArrowDown | Key::ArrowLeft => {
191 (set_alpha)((alpha.get() - 0.01).clamp(0.0, 1.0));
192 EventResponse::Handled
193 }
194 Key::PageUp => {
195 (set_alpha)((alpha.get() + 0.10).clamp(0.0, 1.0));
196 EventResponse::Handled
197 }
198 Key::PageDown => {
199 (set_alpha)((alpha.get() - 0.10).clamp(0.0, 1.0));
200 EventResponse::Handled
201 }
202 Key::Home => {
203 (set_alpha)(0.0);
204 EventResponse::Handled
205 }
206 Key::End => {
207 (set_alpha)(1.0);
208 EventResponse::Handled
209 }
210 _ => EventResponse::Ignored,
211 }
212 });
213 }
214
215 {
216 let focus_origin = self.focus_origin.clone();
217 handlers = handlers.on_focus(move |gained, _ctx| {
218 focus_origin.set(if gained {
219 Some(FocusOrigin::Keyboard)
220 } else {
221 None
222 });
223 });
224 }
225
226 {
227 let set_alpha = set_alpha.clone();
228 let alpha = self.alpha.clone();
229 handlers = handlers.on_access_action(move |action, _ctx| match action {
230 Action::Increment => {
231 (set_alpha)((alpha.get() + 0.01).clamp(0.0, 1.0));
232 EventResponse::Handled
233 }
234 Action::Decrement => {
235 (set_alpha)((alpha.get() - 0.01).clamp(0.0, 1.0));
236 EventResponse::Handled
237 }
238 _ => EventResponse::Ignored,
239 });
240 }
241
242 ctx.apply_self_handlers(handlers);
243 Vec::new()
244 }
245
246 fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
247 use crate::styles::recipe_color_picker_style as cp;
248 let size = match self.orientation {
249 Orientation::Vertical => Size::new(cp::STRIP_THICKNESS, cp::STRIP_LENGTH),
250 Orientation::Horizontal => Size::new(cp::STRIP_LENGTH, cp::STRIP_THICKNESS),
251 };
252 size.into()
253 }
254
255 fn place_children(
256 &self,
257 bounds: Rect,
258 _proposal: SizeProposal,
259 _children: &mut [WidgetPlacement],
260 _ctx: &LayoutContext,
261 ) {
262 self.cached_bounds.set(bounds);
263 }
264
265 fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
266 use crate::styles::recipe_color_picker_style as cp;
267 self.cached_bounds.set(bounds);
268 let radius = CornerRadius::uniform(cp::STRIP_CORNER_RADIUS);
269
270 paint_checkerboard(
275 canvas,
276 bounds,
277 cp::CHECKER_CELL,
278 cp::CHECKER_COLOR_A,
279 cp::CHECKER_COLOR_B,
280 );
281
282 let opaque = self.current_color.get().with_alpha(1.0);
287 let transparent = opaque.with_alpha(0.0);
288 let (start, end) = match self.orientation {
289 Orientation::Vertical => (Point::new(0.0, 0.0), Point::new(0.0, bounds.height)),
290 Orientation::Horizontal => (Point::new(0.0, 0.0), Point::new(bounds.width, 0.0)),
291 };
292 canvas.fill_rounded_rect(
293 bounds,
294 radius,
295 Paint::LinearGradient {
296 start,
297 end,
298 stops: vec![
299 GradientStop {
300 offset: 0.0,
301 color: transparent,
302 },
303 GradientStop {
304 offset: 1.0,
305 color: opaque,
306 },
307 ],
308 },
309 );
310
311 canvas.stroke_rounded_rect(bounds, radius, ctx.theme.colors.border, 1.0);
313
314 let t = self.alpha.get().clamp(0.0, 1.0);
316 let thumb_w = cp::STRIP_THUMB_WIDTH;
317 let thumb_h = cp::STRIP_THUMB_HEIGHT;
318 let thumb_radius = CornerRadius::uniform(cp::STRIP_THUMB_CORNER_RADIUS);
319 let thumb_rect = match self.orientation {
320 Orientation::Vertical => Rect::new(
321 bounds.x - 2.0,
322 bounds.y + bounds.height * t - thumb_h * 0.5,
323 bounds.width + 4.0,
324 thumb_h,
325 ),
326 Orientation::Horizontal => Rect::new(
327 bounds.x + bounds.width * t - thumb_w * 0.5,
328 bounds.y - 2.0,
329 thumb_w,
330 bounds.height + 4.0,
331 ),
332 };
333 canvas.fill_rounded_rect(thumb_rect, thumb_radius, Color::WHITE);
334 canvas.stroke_rounded_rect(
335 thumb_rect,
336 thumb_radius,
337 Color::new(0.0, 0.0, 0.0, 0.5),
338 1.0,
339 );
340 }
341
342 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
343 builder.set_role(Role::Slider);
344 if !self.label.is_empty() {
345 builder.set_name(&self.label);
346 }
347 builder.set_numeric_value((self.alpha.get() * 100.0) as f64);
348 builder.set_min_numeric_value(0.0);
349 builder.set_max_numeric_value(100.0);
350 builder.set_numeric_value_step(1.0);
351 builder.set_numeric_value_jump(10.0);
352 let orientation = match self.orientation {
353 Orientation::Vertical => teksilo_core::accesskit::Orientation::Vertical,
354 Orientation::Horizontal => teksilo_core::accesskit::Orientation::Horizontal,
355 };
356 builder.set_orientation(orientation);
357 builder.add_action(Action::Increment);
359 builder.add_action(Action::Decrement);
360 builder.add_action(Action::Focus);
361 }
362}
363
364pub(crate) fn paint_checkerboard(
368 canvas: &mut Canvas,
369 bounds: Rect,
370 cell: f32,
371 color_a: Color,
372 color_b: Color,
373) {
374 if cell <= 0.0 || bounds.width <= 0.0 || bounds.height <= 0.0 {
375 return;
376 }
377 let cols = (bounds.width / cell).ceil() as i32;
378 let rows = (bounds.height / cell).ceil() as i32;
379 for row in 0..rows {
380 for col in 0..cols {
381 let dark = (row + col) & 1 == 1;
382 let color = if dark { color_a } else { color_b };
383 let x = bounds.x + col as f32 * cell;
384 let y = bounds.y + row as f32 * cell;
385 let w = (bounds.x + bounds.width - x).min(cell);
386 let h = (bounds.y + bounds.height - y).min(cell);
387 if w > 0.0 && h > 0.0 {
388 canvas.fill_rect(Rect::new(x, y, w, h), color);
389 }
390 }
391 }
392}