1use std::cell::Cell;
2use std::rc::Rc;
3
4use sequoia_core::{Color, Edges, Length, Point, Rect, Size};
5use sequoia_input::{CursorIcon, Event, EventPhase, PointerButton, PointerEvent};
6use sequoia_layout::{Axis, LayoutCtx, LayoutStyle};
7use sequoia_reactive::Signal;
8use sequoia_style::{ResizeHandleStyle, VisualState};
9use sequoia_view::{EventCtx, PaintCtx, Unit, View};
10
11const DEFAULT_HIT: f32 = 7.0;
12
13const LINE_IDLE: f32 = 1.0;
14
15const LINE_ACTIVE: f32 = 2.0;
16
17const IDLE_COLOR: Color = Color::rgb8(0x75, 0x75, 0x75);
18
19const ACTIVE_COLOR: Color = Color::rgb8(0x0D, 0x99, 0xFF);
20
21#[derive(Clone, Copy)]
22struct DragState {
23 origin: f32,
24 value: f32,
25}
26
27#[derive(Clone)]
28pub struct ResizeHandle {
29 value: Signal<f32>,
30 min: f32,
31 max: f32,
32 hit: f32,
33 axis: Axis,
34 invert: bool,
35 gutter: Color,
36 style: Option<ResizeHandleStyle>,
37 idle: Option<Color>,
38 active: Option<Color>,
39 idle_width: Option<f32>,
40 active_width: Option<f32>,
41 drag: Rc<Cell<Option<DragState>>>,
42 margin: Edges,
43}
44
45impl ResizeHandle {
46 pub fn new(value: Signal<f32>) -> Self {
47 Self {
48 value,
49 min: 0.0,
50 max: f32::INFINITY,
51 hit: DEFAULT_HIT,
52 axis: Axis::Horizontal,
53 invert: false,
54 gutter: Color::TRANSPARENT,
55 style: None,
56 idle: None,
57 active: None,
58 idle_width: None,
59 active_width: None,
60 drag: Rc::new(Cell::new(None)),
61 margin: Edges::ZERO,
62 }
63 }
64
65 pub fn min(mut self, min: f32) -> Self {
66 self.min = if min.is_finite() { min.max(0.0) } else { 0.0 };
67 self
68 }
69
70 pub fn max(mut self, max: f32) -> Self {
71 self.max = if max.is_nan() {
72 f32::INFINITY
73 } else {
74 max.max(0.0)
75 };
76 self
77 }
78
79 pub fn hit_width(mut self, hit: f32) -> Self {
80 self.hit = if hit.is_finite() {
81 hit.max(3.0)
82 } else {
83 DEFAULT_HIT
84 };
85 self
86 }
87
88 pub fn axis(mut self, axis: Axis) -> Self {
89 self.axis = axis;
90 self
91 }
92
93 pub fn invert(mut self, invert: bool) -> Self {
94 self.invert = invert;
95 self
96 }
97
98 pub fn gutter(mut self, color: Color) -> Self {
99 self.gutter = color;
100 self
101 }
102
103 pub fn style(mut self, style: ResizeHandleStyle) -> Self {
104 self.style = Some(style);
105 self
106 }
107
108 pub fn idle_color(mut self, color: Color) -> Self {
109 self.idle = Some(color);
110 self
111 }
112
113 pub fn active_color(mut self, color: Color) -> Self {
114 self.active = Some(color);
115 self
116 }
117
118 pub fn idle_width(mut self, width: f32) -> Self {
119 self.idle_width = Some(width);
120 self
121 }
122
123 pub fn active_width(mut self, width: f32) -> Self {
124 self.active_width = Some(width);
125 self
126 }
127
128 pub fn margin(mut self, margin: impl Into<Edges>) -> Self {
129 let m = margin.into();
130 self.margin = Edges::only(
131 sanitize_margin(m.left),
132 sanitize_margin(m.top),
133 sanitize_margin(m.right),
134 sanitize_margin(m.bottom),
135 );
136 self
137 }
138
139 pub fn margin_xy(self, x: f32, y: f32) -> Self {
140 self.margin(Edges::xy(x, y))
141 }
142
143 pub fn margin_x(mut self, x: f32) -> Self {
144 let x = sanitize_margin(x);
145 self.margin.left = x;
146 self.margin.right = x;
147 self
148 }
149
150 pub fn margin_y(mut self, y: f32) -> Self {
151 let y = sanitize_margin(y);
152 self.margin.top = y;
153 self.margin.bottom = y;
154 self
155 }
156
157 pub fn margin_left(mut self, v: f32) -> Self {
158 self.margin.left = sanitize_margin(v);
159 self
160 }
161
162 pub fn margin_right(mut self, v: f32) -> Self {
163 self.margin.right = sanitize_margin(v);
164 self
165 }
166
167 pub fn margin_top(mut self, v: f32) -> Self {
168 self.margin.top = sanitize_margin(v);
169 self
170 }
171
172 pub fn margin_bottom(mut self, v: f32) -> Self {
173 self.margin.bottom = sanitize_margin(v);
174 self
175 }
176
177 fn resolved_gutter(&self) -> Color {
178 if self.gutter != Color::TRANSPARENT {
179 self.gutter
180 } else {
181 self.style
182 .unwrap_or_default()
183 .gutter
184 .unwrap_or(Color::TRANSPARENT)
185 }
186 }
187
188 fn resolved_idle(&self) -> Color {
189 let style = self.style.unwrap_or_default();
190 self.idle.or(style.idle).unwrap_or(IDLE_COLOR)
191 }
192
193 fn resolved_active(&self) -> Color {
194 let style = self.style.unwrap_or_default();
195 self.active.or(style.active).unwrap_or(ACTIVE_COLOR)
196 }
197
198 fn resolved_idle_width(&self) -> f32 {
199 let style = self.style.unwrap_or_default();
200 self.idle_width.or(style.idle_width).unwrap_or(LINE_IDLE)
201 }
202
203 fn resolved_active_width(&self) -> f32 {
204 let style = self.style.unwrap_or_default();
205 self.active_width
206 .or(style.active_width)
207 .unwrap_or(LINE_ACTIVE)
208 }
209
210 fn clamped(&self, v: f32) -> f32 {
211 v.clamp(self.min, self.max)
212 }
213
214 fn axis_pos(&self, pos: &Point) -> f32 {
215 match self.axis {
216 Axis::Horizontal => pos.x,
217 Axis::Vertical => pos.y,
218 }
219 }
220}
221
222impl View for ResizeHandle {
223 fn reuse_state(&self, old: &Self) {
224 self.drag.set(old.drag.get());
225 }
226
227 fn body(&self) -> impl View + 'static {
228 Unit
229 }
230
231 fn is_leaf(&self) -> bool {
232 true
233 }
234
235 fn captures_pointer(&self) -> bool {
236 true
237 }
238
239 fn has_visual_state(&self) -> bool {
240 true
241 }
242
243 fn cursor_icon(&self, _state: VisualState) -> CursorIcon {
244 match self.axis {
245 Axis::Horizontal => CursorIcon::ColResize,
246 Axis::Vertical => CursorIcon::RowResize,
247 }
248 }
249
250 fn layout_style(&self) -> LayoutStyle {
251 match self.axis {
252 Axis::Horizontal => LayoutStyle {
253 width: Length::Px(self.hit),
254 height: Length::Fill(1.0),
255 margin: self.margin,
256 ..LayoutStyle::default()
257 },
258 Axis::Vertical => LayoutStyle {
259 width: Length::Fill(1.0),
260 height: Length::Px(self.hit),
261 margin: self.margin,
262 ..LayoutStyle::default()
263 },
264 }
265 }
266
267 fn intrinsic_size(&self, _ctx: &mut LayoutCtx) -> Option<Size> {
268 Some(match self.axis {
269 Axis::Horizontal => Size::new(self.hit, 0.0),
270 Axis::Vertical => Size::new(0.0, self.hit),
271 })
272 }
273
274 fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
275 if ctx.phase != EventPhase::Target {
276 return;
277 }
278 match event {
279 Event::Pointer(PointerEvent::Down {
280 button: PointerButton::Left,
281 pos,
282 ..
283 }) => {
284 let p = self.axis_pos(pos);
285 self.drag.set(Some(DragState {
286 origin: p,
287 value: self.value.get_untracked(),
288 }));
289 ctx.capture_pointer();
290 ctx.handled();
291 }
292 Event::Pointer(PointerEvent::Move { pos, .. }) => {
293 if let Some(st) = self.drag.get() {
294 let p = self.axis_pos(pos);
295 let delta = if self.invert {
296 st.origin - p
297 } else {
298 p - st.origin
299 };
300 self.value.set(self.clamped(st.value + delta));
301 ctx.handled();
302 }
303 }
304 Event::Pointer(PointerEvent::Up {
305 button: PointerButton::Left,
306 ..
307 }) => {
308 self.drag.set(None);
309 }
310 Event::Pointer(PointerEvent::Cancel) => {
311 self.drag.set(None);
312 }
313 _ => {}
314 }
315 }
316
317 fn paint_self(&self, ctx: &mut PaintCtx) {
318 let r = ctx.rect;
319 if r.is_empty() {
320 return;
321 }
322
323 let gutter = self.resolved_gutter();
324 if gutter.a > 0.0 {
325 ctx.quad(r, gutter, 0.0);
326 }
327 let active = ctx.state.hovered || ctx.state.pressed || self.drag.get().is_some();
328 let (w, c) = if active {
329 (self.resolved_active_width(), self.resolved_active())
330 } else {
331 (self.resolved_idle_width(), self.resolved_idle())
332 };
333 match self.axis {
334 Axis::Horizontal => {
335 let cx = r.left() + r.size.width * 0.5;
336 ctx.quad(
337 Rect::from_xywh(cx - w * 0.5, r.top(), w, r.size.height),
338 c,
339 0.0,
340 );
341 }
342 Axis::Vertical => {
343 let cy = r.top() + r.size.height * 0.5;
344 ctx.quad(
345 Rect::from_xywh(r.left(), cy - w * 0.5, r.size.width, w),
346 c,
347 0.0,
348 );
349 }
350 }
351 }
352}
353
354fn sanitize_margin(v: f32) -> f32 {
355 if v.is_finite() { v.max(0.0) } else { 0.0 }
356}