teksilo_widgets/color_picker/
hsv_canvas.rs1use std::cell::Cell;
29use std::rc::Rc;
30
31use teksilo_canvas::paint::GradientStop;
32use teksilo_canvas::{Canvas, Paint, Point, Rect, Size, SizeProposal};
33use teksilo_core::accessibility::AccessNodeBuilder;
34use teksilo_core::accesskit::Role;
35use teksilo_core::build_context::BuildContext;
36use teksilo_core::event::PointerButton;
37use teksilo_core::gesture::DragPhase;
38use teksilo_core::signal::Signal;
39use teksilo_core::widget::{
40 CursorIcon, LayoutContext, LayoutResponse, PaintContext, Widget, WidgetPlacement,
41};
42use teksilo_core::widget_builder::HandlerSet;
43use teksilo_core::widget_id::WidgetId;
44use teksilo_tokens::{Color, CornerRadius};
45
46pub(crate) struct HsvCanvas {
47 hue: Signal<f32>,
48 saturation: Signal<f32>,
49 value_hsv: Signal<f32>,
50 set_hsv: Rc<dyn Fn(f32, f32, f32)>,
51 dragging: Rc<Cell<bool>>,
52 cached_bounds: Rc<Cell<Rect>>,
53 initial_enabled: bool,
55}
56
57impl HsvCanvas {
58 pub(crate) fn new(
59 hue: Signal<f32>,
60 saturation: Signal<f32>,
61 value_hsv: Signal<f32>,
62 set_hsv: Rc<dyn Fn(f32, f32, f32)>,
63 dragging: Rc<Cell<bool>>,
64 ) -> Self {
65 Self {
66 hue,
67 saturation,
68 value_hsv,
69 set_hsv,
70 dragging,
71 cached_bounds: Rc::new(Cell::new(Rect::ZERO)),
72 initial_enabled: true,
73 }
74 }
75
76 pub(crate) fn enabled(mut self, enabled: bool) -> Self {
78 self.initial_enabled = enabled;
79 self
80 }
81}
82
83impl std::fmt::Debug for HsvCanvas {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 f.debug_struct("HsvCanvas").finish_non_exhaustive()
86 }
87}
88
89impl Widget for HsvCanvas {
90 fn build(&mut self, ctx: &mut BuildContext) -> Vec<WidgetId> {
91 let self_id = ctx.self_id();
92 if !self.initial_enabled {
94 ctx.enabled_when(self_id, false);
95 }
96 let registry = ctx.binding_registry();
97 self.hue.bind_to(
100 self_id,
101 registry,
102 teksilo_core::binding::BindingLevel::RepaintOnly,
103 );
104 self.saturation.bind_to(
105 self_id,
106 registry,
107 teksilo_core::binding::BindingLevel::RepaintOnly,
108 );
109 self.value_hsv.bind_to(
110 self_id,
111 registry,
112 teksilo_core::binding::BindingLevel::RepaintOnly,
113 );
114
115 let cached_bounds = self.cached_bounds.clone();
118 let dragging = self.dragging.clone();
119 let set_hsv = self.set_hsv.clone();
120 let hue_for_drag = self.hue.clone();
121
122 let apply: Rc<dyn Fn(f32, f32)> = {
123 let set_hsv = set_hsv.clone();
124 Rc::new(move |x: f32, y: f32| {
125 let bounds = cached_bounds.get();
126 if bounds.width <= 0.0 || bounds.height <= 0.0 {
127 return;
128 }
129 let sat = (x / bounds.width).clamp(0.0, 1.0);
132 let val = (1.0 - y / bounds.height).clamp(0.0, 1.0);
134 (set_hsv)(hue_for_drag.get(), sat, val);
135 })
136 };
137
138 let mut handlers = HandlerSet::new()
139 .focusable(false)
140 .cursor(CursorIcon::Crosshair);
141
142 {
143 let dragging = dragging.clone();
144 let apply = apply.clone();
145 handlers = handlers.on_drag(move |phase, _ctx| match phase {
146 DragPhase::Started {
147 position,
148 button: PointerButton::Primary,
149 } => {
150 dragging.set(true);
151 apply(position.x, position.y);
152 }
153 DragPhase::Moved { position, .. } if dragging.get() => {
154 apply(position.x, position.y);
155 }
156 DragPhase::Ended { .. } => {
157 dragging.set(false);
158 }
159 _ => {}
160 });
161 }
162 {
163 let apply = apply.clone();
164 handlers = handlers.on_tap(move |event, _ctx| {
165 apply(event.position.x, event.position.y);
166 });
167 }
168
169 ctx.apply_self_handlers(handlers);
170 Vec::new()
171 }
172
173 fn layout_response(&self, _proposal: SizeProposal, _ctx: &LayoutContext) -> LayoutResponse {
174 use crate::styles::recipe_color_picker_style as cp;
175 Size::new(cp::CANVAS_WIDTH, cp::CANVAS_HEIGHT).into()
176 }
177
178 fn place_children(
179 &self,
180 bounds: Rect,
181 _proposal: SizeProposal,
182 _children: &mut [WidgetPlacement],
183 _ctx: &LayoutContext,
184 ) {
185 self.cached_bounds.set(bounds);
186 }
187
188 fn paint(&self, bounds: Rect, canvas: &mut Canvas, _ctx: &PaintContext) {
189 use crate::styles::recipe_color_picker_style as cp;
190 self.cached_bounds.set(bounds);
191 let radius = CornerRadius::uniform(cp::CANVAS_CORNER_RADIUS);
192
193 let hue = self.hue.get();
195 let base = Color::from_hsv(hue, 1.0, 1.0);
196 canvas.fill_rounded_rect(bounds, radius, base);
197
198 canvas.fill_rounded_rect(
203 bounds,
204 radius,
205 Paint::LinearGradient {
206 start: Point::new(0.0, 0.0),
207 end: Point::new(bounds.width, 0.0),
208 stops: vec![
209 GradientStop {
210 offset: 0.0,
211 color: Color::WHITE,
212 },
213 GradientStop {
214 offset: 1.0,
215 color: Color::WHITE.with_alpha(0.0),
216 },
217 ],
218 },
219 );
220
221 canvas.fill_rounded_rect(
223 bounds,
224 radius,
225 Paint::LinearGradient {
226 start: Point::new(0.0, 0.0),
227 end: Point::new(0.0, bounds.height),
228 stops: vec![
229 GradientStop {
230 offset: 0.0,
231 color: Color::BLACK.with_alpha(0.0),
232 },
233 GradientStop {
234 offset: 1.0,
235 color: Color::BLACK,
236 },
237 ],
238 },
239 );
240
241 let sat = self.saturation.get().clamp(0.0, 1.0);
243 let val = self.value_hsv.get().clamp(0.0, 1.0);
244 let cx = bounds.x + bounds.width * sat;
245 let cy = bounds.y + bounds.height * (1.0 - val);
246 let center = Point::new(cx, cy);
247
248 canvas.stroke_circle(
249 center,
250 cp::INDICATOR_RADIUS + cp::INDICATOR_INNER_STROKE_WIDTH,
251 cp::INDICATOR_OUTER_COLOR,
252 cp::INDICATOR_OUTER_STROKE_WIDTH,
253 );
254 canvas.stroke_circle(
255 center,
256 cp::INDICATOR_RADIUS,
257 cp::INDICATOR_INNER_COLOR,
258 cp::INDICATOR_INNER_STROKE_WIDTH,
259 );
260 }
261
262 fn accessibility(&self, builder: &mut AccessNodeBuilder) {
263 builder.set_role(Role::GenericContainer);
268 }
269}