radix_leptos_primitives/components/
resizable.rs1use leptos::callback::Callback;
2use leptos::children::Children;
3use leptos::prelude::*;
4
5#[component]
7pub fn Resizable(
8 #[prop(optional)]
10 width: Option<f64>,
11 #[prop(optional)]
13 height: Option<f64>,
14 #[prop(optional)]
16 min_width: Option<f64>,
17 #[prop(optional)]
19 min_height: Option<f64>,
20 #[prop(optional)]
22 max_width: Option<f64>,
23 #[prop(optional)]
25 max_height: Option<f64>,
26 #[prop(optional)]
28 enabled: Option<bool>,
29 #[prop(optional)]
31 handles: Option<Vec<ResizeHandle>>,
32 #[prop(optional)]
34 maintain_aspect_ratio: Option<bool>,
35 #[prop(optional)]
37 aspect_ratio: Option<f64>,
38 #[prop(optional)]
40 on_resize_start: Option<Callback<ResizeEvent>>,
41 #[prop(optional)]
43 on_resize: Option<Callback<ResizeEvent>>,
44 #[prop(optional)]
46 on_resize_end: Option<Callback<ResizeEvent>>,
47 #[prop(optional)]
49 class: Option<String>,
50 #[prop(optional)]
52 style: Option<String>,
53 children: Option<Children>,
55) -> impl IntoView {
56 let width = width.unwrap_or(200.0);
57 let height = height.unwrap_or(200.0);
58 let min_width = min_width.unwrap_or(50.0);
59 let min_height = min_height.unwrap_or(50.0);
60 let max_width = max_width.unwrap_or(f64::INFINITY);
61 let max_height = max_height.unwrap_or(f64::INFINITY);
62 let enabled = enabled.unwrap_or(true);
63 let handles = handles.unwrap_or_else(|| [ResizeHandle::BottomRight].to_vec());
64 let maintain_aspect_ratio = maintain_aspect_ratio.unwrap_or(false);
65 let aspect_ratio = aspect_ratio.unwrap_or(1.0);
66
67 let class = format!(
68 "resizable {} {} {} {} {} {} {}",
69 width,
70 height,
71 min_width,
72 min_height,
73 max_width,
74 max_height,
75 style.as_ref().unwrap_or(&String::new())
76 );
77
78 let handle_resize_start = move |event: web_sys::MouseEvent| {
79 if enabled {
80 let resize_event = ResizeEvent {
81 width,
82 height,
83 delta_x: 0.0,
84 delta_y: 0.0,
85 handle: ResizeHandle::BottomRight,
86 };
87 if let Some(callback) = on_resize_start {
88 callback.run(resize_event);
89 }
90 }
91 };
92
93 let handle_resize = move |event: web_sys::MouseEvent| {
94 if enabled {
95 let resize_event = ResizeEvent {
96 width: width + event.client_x() as f64,
97 height: height + event.client_y() as f64,
98 delta_x: event.client_x() as f64,
99 delta_y: event.client_y() as f64,
100 handle: ResizeHandle::BottomRight,
101 };
102 if let Some(callback) = on_resize {
103 callback.run(resize_event);
104 }
105 }
106 };
107
108 let handle_resize_end = move |event: web_sys::MouseEvent| {
109 if enabled {
110 let resize_event = ResizeEvent {
111 width: width + event.client_x() as f64,
112 height: height + event.client_y() as f64,
113 delta_x: event.client_x() as f64,
114 delta_y: event.client_y() as f64,
115 handle: ResizeHandle::BottomRight,
116 };
117 if let Some(callback) = on_resize_end {
118 callback.run(resize_event);
119 }
120 }
121 };
122
123 view! {
124 <div class=class style=style>
125 {children.map(|c| c())}
126 {if enabled {
127 view! {
128 <div class="resize-handles">
129 {handles.into_iter().map(|handle| {
130 view! {
131 <ResizeHandle
132 handle=handle
133 on_resize_start=on_resize_start.unwrap_or_else(|| Callback::new(|_| {}))
134 on_resize=on_resize.unwrap_or_else(|| Callback::new(|_| {}))
135 on_resize_end=on_resize_end.unwrap_or_else(|| Callback::new(|_| {}))
136 />
137 }
138 }).collect::<Vec<_>>()}
139 </div>
140 }.into_any()
141 } else {
142 view! { <div></div> }.into_any()
143 }}
144 </div>
145 }
146}
147
148#[component]
150pub fn ResizeHandle(
151 handle: ResizeHandle,
153 #[prop(optional)]
155 on_resize_start: Option<Callback<ResizeEvent>>,
156 #[prop(optional)]
158 on_resize: Option<Callback<ResizeEvent>>,
159 #[prop(optional)]
161 on_resize_end: Option<Callback<ResizeEvent>>,
162 #[prop(optional)]
164 class: Option<String>,
165 #[prop(optional)]
167 style: Option<String>,
168) -> impl IntoView {
169 let class = format!(
170 "resize-handle {} {}",
171 match handle {
172 ResizeHandle::Top => "top",
173 ResizeHandle::Right => "right",
174 ResizeHandle::Bottom => "bottom",
175 ResizeHandle::Left => "left",
176 ResizeHandle::TopLeft => "top-left",
177 ResizeHandle::TopRight => "top-right",
178 ResizeHandle::BottomLeft => "bottom-left",
179 ResizeHandle::BottomRight => "bottom-right",
180 },
181 class.unwrap_or_default()
182 );
183
184 let style = style.unwrap_or_default();
185
186 let handle_resize_start = move |event: web_sys::MouseEvent| {
187 let resize_event = ResizeEvent {
188 width: 0.0,
189 height: 0.0,
190 delta_x: event.client_x() as f64,
191 delta_y: event.client_y() as f64,
192 handle,
193 };
194 if let Some(callback) = on_resize_start {
195 callback.run(resize_event);
196 }
197 };
198
199 let handle_resize = move |event: web_sys::MouseEvent| {
200 let resize_event = ResizeEvent {
201 width: 0.0,
202 height: 0.0,
203 delta_x: event.client_x() as f64,
204 delta_y: event.client_y() as f64,
205 handle,
206 };
207 if let Some(callback) = on_resize {
208 callback.run(resize_event);
209 }
210 };
211
212 let handle_resize_end = move |event: web_sys::MouseEvent| {
213 let resize_event = ResizeEvent {
214 width: 0.0,
215 height: 0.0,
216 delta_x: event.client_x() as f64,
217 delta_y: event.client_y() as f64,
218 handle,
219 };
220 if let Some(callback) = on_resize_end {
221 callback.run(resize_event);
222 }
223 };
224
225 view! {
226 <div
227 class=class
228 style=style
229 on:mousedown=handle_resize_start
230 on:mousemove=handle_resize
231 on:mouseup=handle_resize_end
232 />
233 }
234}
235
236#[derive(Debug, Clone, Copy, PartialEq, Default)]
238pub enum ResizeHandle {
239 #[default]
240 BottomRight,
241 Top,
242 Right,
243 Bottom,
244 Left,
245 TopLeft,
246 TopRight,
247 BottomLeft,
248}
249
250#[derive(Debug, Clone, PartialEq, Default)]
252pub struct ResizeEvent {
253 pub width: f64,
254 pub height: f64,
255 pub delta_x: f64,
256 pub delta_y: f64,
257 pub handle: ResizeHandle,
258}
259
260#[component]
262pub fn ResizablePanel(
263 #[prop(optional)]
265 content: Option<String>,
266 #[prop(optional)]
268 title: Option<String>,
269 #[prop(optional)]
271 collapsible: Option<bool>,
272 #[prop(optional)]
274 collapsed: Option<bool>,
275 #[prop(optional)]
277 on_toggle: Option<Callback<bool>>,
278 #[prop(optional)]
280 class: Option<String>,
281 #[prop(optional)]
283 style: Option<String>,
284 children: Option<Children>,
286) -> impl IntoView {
287 let content = content.unwrap_or_default();
288 let title = title.unwrap_or_default();
289 let collapsible = collapsible.unwrap_or(false);
290 let collapsed = collapsed.unwrap_or(false);
291
292 let class = "resizable-panel".to_string();
293
294 let style = style.unwrap_or_default();
295
296 let handle_toggle = move |_| {
297 if collapsible {
298 if let Some(callback) = on_toggle {
299 callback.run(!collapsed);
300 }
301 }
302 };
303
304 view! {
305 <div class=class style=style>
306 {if !title.is_empty() {
307 view! {
308 <div class="panel-header">
309 <h3 class="panel-title">{title}</h3>
310 {if collapsible {
311 view! {
312 <button
313 class="panel-toggle"
314 type="button"
315 on:click=handle_toggle
316 >
317 {if collapsed {
318 "Expand"
319 } else {
320 "Collapse"
321 }}
322 </button>
323 }.into_any()
324 } else {
325 view! { <div></div> }.into_any()
326 }}
327 </div>
328 }.into_any()
329 } else {
330 view! { <div></div> }.into_any()
331 }}
332 {if !collapsed {
333 view! {
334 <div class="panel-content">
335 {if !content.is_empty() {
336 view! { <div class="panel-text">{content}</div> }.into_any()
337 } else {
338 view! { <div></div> }.into_any()
339 }}
340 {children.map(|c| c())}
341 </div>
342 }.into_any()
343 } else {
344 view! { <div></div> }.into_any()
345 }}
346 </div>
347 }
348}
349
350#[component]
352pub fn ResizableSplitter(
353 #[prop(optional)]
355 orientation: Option<SplitterOrientation>,
356 #[prop(optional)]
358 position: Option<f64>,
359 #[prop(optional)]
361 min_position: Option<f64>,
362 #[prop(optional)]
364 max_position: Option<f64>,
365 #[prop(optional)]
367 on_position_change: Option<Callback<f64>>,
368 #[prop(optional)]
370 class: Option<String>,
371 #[prop(optional)]
373 style: Option<String>,
374) -> impl IntoView {
375 let orientation = orientation.unwrap_or_default();
376 let position = position.unwrap_or(0.5);
377 let min_position = min_position.unwrap_or(0.1);
378 let max_position = max_position.unwrap_or(0.9);
379
380 let class = format!(
381 "resizable-splitter {} {}",
382 match orientation {
383 SplitterOrientation::Horizontal => "horizontal",
384 SplitterOrientation::Vertical => "vertical",
385 },
386 class.unwrap_or_default()
387 );
388
389 let style = format!(
390 "{}: {}%; {}",
391 match orientation {
392 SplitterOrientation::Horizontal => "top",
393 SplitterOrientation::Vertical => "left",
394 },
395 position * 100.0,
396 style.unwrap_or_default()
397 );
398
399 let handle_drag = move |event: web_sys::MouseEvent| {
400 let new_position: f64 = match orientation {
401 SplitterOrientation::Horizontal => {
402 0.5
404 }
405 SplitterOrientation::Vertical => {
406 0.5
408 }
409 };
410
411 let clamped_position = new_position.clamp(min_position, max_position);
412 if let Some(callback) = on_position_change {
413 callback.run(clamped_position);
414 }
415 };
416
417 view! {
418 <div
419 class=class
420 style=style
421 on:mousedown=handle_drag
422 />
423 }
424}
425
426#[derive(Debug, Clone, Copy, PartialEq, Default)]
428pub enum SplitterOrientation {
429 #[default]
430 Vertical,
431 Horizontal,
432}
433
434#[cfg(test)]
435mod tests {
436 use crate::{ResizeEvent, ResizeHandle, SplitterOrientation};
437use crate::utils::{merge_optional_classes, generate_id};
438
439 #[test]
441 fn test_resizable_component_creation() {}
442
443 #[test]
444 fn test_resize_handle_component_creation() {}
445
446 #[test]
447 fn test_resizable_panel_component_creation() {}
448
449 #[test]
450 fn test_resizable_splitter_component_creation() {}
451
452 #[test]
454 fn test_resize_handle_enum() {
455 assert_eq!(ResizeHandle::BottomRight, ResizeHandle::default());
456 assert_eq!(ResizeHandle::Top, ResizeHandle::Top);
457 assert_eq!(ResizeHandle::Right, ResizeHandle::Right);
458 assert_eq!(ResizeHandle::Bottom, ResizeHandle::Bottom);
459 assert_eq!(ResizeHandle::Left, ResizeHandle::Left);
460 assert_eq!(ResizeHandle::TopLeft, ResizeHandle::TopLeft);
461 assert_eq!(ResizeHandle::TopRight, ResizeHandle::TopRight);
462 assert_eq!(ResizeHandle::BottomLeft, ResizeHandle::BottomLeft);
463 }
464
465 #[test]
466 fn test_resize_event_struct() {
467 let event = ResizeEvent {
468 width: 100.0,
469 height: 200.0,
470 delta_x: 10.0,
471 delta_y: 20.0,
472 handle: ResizeHandle::BottomRight,
473 };
474 assert_eq!(event.width, 100.0);
475 assert_eq!(event.height, 200.0);
476 assert_eq!(event.delta_x, 10.0);
477 assert_eq!(event.delta_y, 20.0);
478 assert_eq!(event.handle, ResizeHandle::BottomRight);
479 }
480
481 #[test]
482 fn test_resize_event_default() {
483 let event = ResizeEvent::default();
484 assert_eq!(event.width, 0.0);
485 assert_eq!(event.height, 0.0);
486 assert_eq!(event.delta_x, 0.0);
487 assert_eq!(event.delta_y, 0.0);
488 assert_eq!(event.handle, ResizeHandle::BottomRight);
489 }
490
491 #[test]
492 fn test_splitter_orientation_enum() {
493 assert_eq!(
494 SplitterOrientation::Vertical,
495 SplitterOrientation::default()
496 );
497 assert_eq!(
498 SplitterOrientation::Horizontal,
499 SplitterOrientation::Horizontal
500 );
501 assert_eq!(SplitterOrientation::Vertical, SplitterOrientation::Vertical);
502 }
503
504 #[test]
506 fn test_resizable_props_handling() {}
507
508 #[test]
509 fn test_resizable_dimensions() {}
510
511 #[test]
512 fn test_resizable_constraints() {}
513
514 #[test]
515 fn test_resizable_enabled_state() {}
516
517 #[test]
518 fn test_resizable_handles() {}
519
520 #[test]
521 fn test_resizable_aspect_ratio() {}
522
523 #[test]
525 fn test_resizable_resize_start() {}
526
527 #[test]
528 fn test_resizable_resize() {}
529
530 #[test]
531 fn test_resizable_resize_end() {}
532
533 #[test]
534 fn test_resize_handle_events() {}
535
536 #[test]
538 fn test_resizable_panel_content() {}
539
540 #[test]
541 fn test_resizable_panel_title() {}
542
543 #[test]
544 fn test_resizable_panel_collapsible() {}
545
546 #[test]
547 fn test_resizable_panel_collapsed() {}
548
549 #[test]
550 fn test_resizable_panel_toggle() {}
551
552 #[test]
554 fn test_resizable_splitter_orientation() {}
555
556 #[test]
557 fn test_resizable_splitter_position() {}
558
559 #[test]
560 fn test_resizable_splitter_constraints() {}
561
562 #[test]
563 fn test_resizable_splitter_drag() {}
564
565 #[test]
567 fn test_resizable_min_width_constraint() {}
568
569 #[test]
570 fn test_resizable_min_height_constraint() {}
571
572 #[test]
573 fn test_resizable_max_width_constraint() {}
574
575 #[test]
576 fn test_resizable_max_height_constraint() {}
577
578 #[test]
580 fn test_resizable_maintain_aspect_ratio() {}
581
582 #[test]
583 fn test_resizable_custom_aspect_ratio() {}
584
585 #[test]
587 fn test_resize_handle_positioning() {}
588
589 #[test]
590 fn test_resize_handle_cursor_styles() {}
591
592 #[test]
594 fn test_resizable_accessibility() {}
595
596 #[test]
597 fn test_resizable_keyboard_navigation() {}
598
599 #[test]
600 fn test_resizable_screen_reader_support() {}
601
602 #[test]
604 fn test_resizable_performance() {}
605
606 #[test]
607 fn test_resizable_large_content() {}
608
609 #[test]
611 fn test_resizable_full_workflow() {}
612
613 #[test]
614 fn test_resizable_with_panel() {}
615
616 #[test]
617 fn test_resizable_with_splitter() {}
618
619 #[test]
621 fn test_resizable_zero_dimensions() {}
622
623 #[test]
624 fn test_resizable_negative_dimensions() {}
625
626 #[test]
627 fn test_resizable_invalid_constraints() {}
628
629 #[test]
631 fn test_resizable_custom_classes() {}
632
633 #[test]
634 fn test_resizable_custom_styles() {}
635
636 #[test]
637 fn test_resizable_responsive_design() {}
638}