1use 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, PointerButton, WidgetEvent};
27use teksilo_core::focus::FocusOrigin;
28use teksilo_core::gesture::DragPhase;
29use teksilo_core::pointer::touch_action::TouchAction;
30use teksilo_core::signal::Signal;
31use teksilo_core::widget::{
32 CursorIcon, LayoutContext, LayoutResponse, PaintContext, Widget, WidgetPlacement,
33};
34use teksilo_core::widget_builder::HandlerSet;
35use teksilo_core::widget_id::WidgetId;
36use teksilo_tokens::{Color, CornerRadius, Orientation};
37
38use crate::common::range_nav::{self, RangeAxis, RangeKind, RangeMove};
39
40pub(crate) struct AlphaStrip {
41 current_color: Signal<Color>,
43 alpha: Signal<f32>,
44 set_alpha: Rc<dyn Fn(f32)>,
45 dragging: Rc<Cell<bool>>,
46 cached_bounds: Rc<Cell<Rect>>,
47 focus_origin: Rc<Cell<Option<FocusOrigin>>>,
48 orientation: Orientation,
49 initial_enabled: bool,
51 label: String,
52}
53
54impl AlphaStrip {
55 pub(crate) fn new(
56 current_color: Signal<Color>,
57 alpha: Signal<f32>,
58 set_alpha: Rc<dyn Fn(f32)>,
59 dragging: Rc<Cell<bool>>,
60 ) -> Self {
61 Self {
62 current_color,
63 alpha,
64 set_alpha,
65 dragging,
66 cached_bounds: Rc::new(Cell::new(Rect::ZERO)),
67 focus_origin: Rc::new(Cell::new(None)),
68 orientation: Orientation::Vertical,
69 initial_enabled: true,
70 label: String::new(),
71 }
72 }
73
74 pub(crate) fn orientation(mut self, orientation: Orientation) -> Self {
75 self.orientation = orientation;
76 self
77 }
78
79 pub(crate) fn enabled(mut self, enabled: bool) -> Self {
81 self.initial_enabled = enabled;
82 self
83 }
84
85 pub(crate) fn label(mut self, label: impl Into<String>) -> Self {
86 self.label = label.into();
87 self
88 }
89}
90
91impl std::fmt::Debug for AlphaStrip {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 f.debug_struct("AlphaStrip").finish_non_exhaustive()
94 }
95}
96
97impl Widget for AlphaStrip {
98 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
99 let self_id = ctx.self_id();
100 if !self.initial_enabled {
102 ctx.enabled_when(self_id, false);
103 }
104 let registry = ctx.binding_registry();
105 self.current_color.bind_to(
107 self_id,
108 registry,
109 teksilo_core::binding::BindingLevel::RepaintOnly,
110 );
111 self.alpha.bind_to(
112 self_id,
113 registry,
114 teksilo_core::binding::BindingLevel::RepaintOnly,
115 );
116
117 let cached_bounds = self.cached_bounds.clone();
120 let dragging = self.dragging.clone();
121 let set_alpha = self.set_alpha.clone();
122 let orientation = self.orientation;
123
124 let apply: Rc<dyn Fn(f32, f32)> = {
125 let set_alpha = set_alpha.clone();
126 Rc::new(move |x: f32, y: f32| {
127 let bounds = cached_bounds.get();
128 let t = match orientation {
131 Orientation::Vertical => {
136 if bounds.height <= 0.0 {
137 return;
138 }
139 1.0 - (y / bounds.height).clamp(0.0, 1.0)
140 }
141 Orientation::Horizontal => {
142 if bounds.width <= 0.0 {
143 return;
144 }
145 (x / bounds.width).clamp(0.0, 1.0)
146 }
147 };
148 (set_alpha)(t.clamp(0.0, 1.0));
151 })
152 };
153
154 let mut handlers = HandlerSet::new()
155 .focusable(true)
156 .cursor(CursorIcon::Pointer)
157 .touch_action(TouchAction::NONE);
166
167 {
168 let dragging = dragging.clone();
169 let apply = apply.clone();
170 handlers = handlers.on_drag(move |phase, _ctx| match phase {
171 DragPhase::Started {
172 position,
173 button: PointerButton::Primary,
174 ..
175 } => {
176 dragging.set(true);
177 apply(position.x, position.y);
178 }
179 DragPhase::Moved { position, .. } if dragging.get() => {
180 apply(position.x, position.y);
181 }
182 DragPhase::Ended { .. } => {
183 dragging.set(false);
184 }
185 _ => {}
186 });
187 }
188 {
189 let apply = apply.clone();
190 handlers = handlers.on_tap(move |event, _ctx| {
191 apply(event.position.x, event.position.y);
192 });
193 }
194
195 {
197 let set_alpha = set_alpha.clone();
198 let alpha = self.alpha.clone();
199 handlers = handlers.on_key(move |event, _ctx| {
200 let WidgetEvent::KeyDown { key, modifiers, .. } = event else {
201 return EventResponse::Ignored;
202 };
203 let Some(mv) = range_nav::range_move(
206 *key,
207 *modifiers,
208 RangeKind::Scalar,
209 RangeAxis::Both,
210 false,
211 ) else {
212 return EventResponse::Ignored;
213 };
214 let next = match mv {
217 RangeMove::Step { increase } => {
218 alpha.get() + if increase { 0.01 } else { -0.01 }
219 }
220 RangeMove::Page { increase } => {
221 alpha.get() + if increase { 0.10 } else { -0.10 }
222 }
223 RangeMove::ToMin => 0.0,
224 RangeMove::ToMax => 1.0,
225 };
226 (set_alpha)(next.clamp(0.0, 1.0));
227 EventResponse::Handled
228 });
229 }
230
231 {
232 let focus_origin = self.focus_origin.clone();
233 handlers = handlers.on_focus(move |gained, _ctx| {
234 focus_origin.set(if gained {
235 Some(FocusOrigin::Keyboard)
236 } else {
237 None
238 });
239 });
240 }
241
242 {
243 let set_alpha = set_alpha.clone();
244 let alpha = self.alpha.clone();
245 handlers = handlers.on_access_action(move |action, _ctx| match action {
246 Action::Increment => {
247 (set_alpha)((alpha.get() + 0.01).clamp(0.0, 1.0));
248 EventResponse::Handled
249 }
250 Action::Decrement => {
251 (set_alpha)((alpha.get() - 0.01).clamp(0.0, 1.0));
252 EventResponse::Handled
253 }
254 _ => EventResponse::Ignored,
255 });
256 }
257
258 ctx.apply_self_handlers(handlers);
259 Vec::new()
260 }
261
262 fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
263 use crate::styles::recipe_color_picker_style as cp;
264 let size = match self.orientation {
265 Orientation::Vertical => Size::new(cp::STRIP_THICKNESS, cp::STRIP_LENGTH),
266 Orientation::Horizontal => Size::new(cp::STRIP_LENGTH, cp::STRIP_THICKNESS),
267 };
268 size.into()
269 }
270
271 fn place_children(
272 &self,
273 bounds: Rect,
274 _proposal: SizeProposal,
275 _children: &mut [WidgetPlacement],
276 _ctx: &LayoutContext,
277 ) {
278 self.cached_bounds.set(bounds);
279 }
280
281 fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
282 use crate::styles::recipe_color_picker_style as cp;
283 self.cached_bounds.set(bounds);
284 let radius = CornerRadius::uniform(cp::STRIP_CORNER_RADIUS);
285
286 paint_checkerboard(
291 canvas,
292 bounds,
293 cp::CHECKER_CELL,
294 cp::CHECKER_COLOR_A,
295 cp::CHECKER_COLOR_B,
296 );
297
298 let opaque = self.current_color.get().with_alpha(1.0);
303 let transparent = opaque.with_alpha(0.0);
304 let (start, end) = match self.orientation {
305 Orientation::Vertical => (Point::new(0.0, bounds.height), Point::new(0.0, 0.0)),
309 Orientation::Horizontal => (Point::new(0.0, 0.0), Point::new(bounds.width, 0.0)),
310 };
311 canvas.fill_rounded_rect(
312 bounds,
313 radius,
314 Paint::LinearGradient {
315 start,
316 end,
317 stops: vec![
318 GradientStop {
319 offset: 0.0,
320 color: transparent,
321 },
322 GradientStop {
323 offset: 1.0,
324 color: opaque,
325 },
326 ],
327 },
328 );
329
330 canvas.stroke_rounded_rect(bounds, radius, ctx.theme.colors.border, 1.0);
332
333 let t = self.alpha.get().clamp(0.0, 1.0);
335 let thumb_w = cp::STRIP_THUMB_WIDTH;
336 let thumb_h = cp::STRIP_THUMB_HEIGHT;
337 let thumb_radius = CornerRadius::uniform(cp::STRIP_THUMB_CORNER_RADIUS);
338 let thumb_rect = match self.orientation {
339 Orientation::Vertical => Rect::new(
341 bounds.x - 2.0,
342 bounds.bottom() - bounds.height * t - thumb_h * 0.5,
343 bounds.width + 4.0,
344 thumb_h,
345 ),
346 Orientation::Horizontal => Rect::new(
347 bounds.x + bounds.width * t - thumb_w * 0.5,
348 bounds.y - 2.0,
349 thumb_w,
350 bounds.height + 4.0,
351 ),
352 };
353 canvas.fill_rounded_rect(thumb_rect, thumb_radius, Color::WHITE);
354 canvas.stroke_rounded_rect(
355 thumb_rect,
356 thumb_radius,
357 Color::new(0.0, 0.0, 0.0, 0.5),
358 1.0,
359 );
360 }
361
362 fn hit_outset(
369 &self,
370 kind: teksilo_tokens::PointerKind,
371 tokens: &teksilo_tokens::InputTokens,
372 ) -> teksilo_canvas::EdgeInsets {
373 crate::button::target_outset(self.cached_bounds.get().size(), kind, tokens)
374 }
375
376 fn target_regions(&self, bounds: Rect) -> Vec<teksilo_core::partition::TargetRegion> {
382 use crate::styles::recipe_color_picker_style as cp;
383 use teksilo_core::partition::TargetRegion;
384
385 let t = self.alpha.get().clamp(0.0, 1.0);
386 let thumb = match self.orientation {
387 Orientation::Vertical => Rect::new(
388 bounds.x - 2.0,
389 bounds.y + bounds.height * t - cp::STRIP_THUMB_HEIGHT * 0.5,
390 bounds.width + 4.0,
391 cp::STRIP_THUMB_HEIGHT,
392 ),
393 Orientation::Horizontal => Rect::new(
394 bounds.x + bounds.width * t - cp::STRIP_THUMB_WIDTH * 0.5,
395 bounds.y - 2.0,
396 cp::STRIP_THUMB_WIDTH,
397 bounds.height + 4.0,
398 ),
399 };
400 vec![
401 TargetRegion::target(bounds, 0),
402 TargetRegion::grab(thumb, 1),
403 ]
404 }
405
406 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
407 builder.set_role(Role::Slider);
408 if !self.label.is_empty() {
409 builder.set_name(&self.label);
410 }
411 builder.set_numeric_value((self.alpha.get() * 100.0) as f64);
412 builder.set_min_numeric_value(0.0);
413 builder.set_max_numeric_value(100.0);
414 builder.set_numeric_value_step(1.0);
415 builder.set_numeric_value_jump(10.0);
416 let orientation = match self.orientation {
417 Orientation::Vertical => teksilo_core::accesskit::Orientation::Vertical,
418 Orientation::Horizontal => teksilo_core::accesskit::Orientation::Horizontal,
419 };
420 builder.set_orientation(orientation);
421 builder.add_action(Action::Increment);
423 builder.add_action(Action::Decrement);
424 builder.add_action(Action::Focus);
425 }
426}
427
428pub(crate) fn paint_checkerboard(
432 canvas: &mut Canvas,
433 bounds: Rect,
434 cell: f32,
435 color_a: Color,
436 color_b: Color,
437) {
438 if cell <= 0.0 || bounds.width <= 0.0 || bounds.height <= 0.0 {
439 return;
440 }
441 let cols = (bounds.width / cell).ceil() as i32;
442 let rows = (bounds.height / cell).ceil() as i32;
443 for row in 0..rows {
444 for col in 0..cols {
445 let dark = (row + col) & 1 == 1;
446 let color = if dark { color_a } else { color_b };
447 let x = bounds.x + col as f32 * cell;
448 let y = bounds.y + row as f32 * cell;
449 let w = (bounds.x + bounds.width - x).min(cell);
450 let h = (bounds.y + bounds.height - y).min(cell);
451 if w > 0.0 && h > 0.0 {
452 canvas.fill_rect(Rect::new(x, y, w, h), color);
453 }
454 }
455 }
456}