use dioxus::prelude::*;
use freya_core::{
custom_attributes::NodeReferenceLayout,
platform::CursorIcon,
};
use freya_elements::{
self as dioxus_elements,
events::MouseEvent,
PointerEvent,
};
use freya_hooks::{
use_applied_theme,
use_node_signal,
use_platform,
ResizableHandleTheme,
ResizableHandleThemeWith,
UseId,
};
#[derive(Clone, Copy, Debug)]
pub struct Panel {
pub size: f32,
pub initial_size: f32,
pub min_size: f32,
pub id: usize,
}
#[derive(Default)]
pub struct ResizableContext {
pub panels: Vec<Panel>,
pub direction: String,
}
impl ResizableContext {
pub fn direction(&self) -> &str {
&self.direction
}
pub fn panels(&mut self) -> &mut Vec<Panel> {
&mut self.panels
}
pub fn push_panel(&mut self, panel: Panel, order: Option<usize>) {
let mut buffer = panel.size;
for panel in &mut self.panels.iter_mut() {
let resized_sized = (panel.initial_size - panel.size).min(buffer);
if resized_sized >= 0. {
panel.size = (panel.size - resized_sized).max(panel.min_size);
let new_resized_sized = panel.initial_size - panel.size;
buffer -= new_resized_sized;
}
}
if let Some(order) = order {
if self.panels.len() <= order {
self.panels.push(panel);
} else {
self.panels.insert(order, panel);
}
} else {
self.panels.push(panel);
}
}
pub fn remove_panel(&mut self, id: usize) {
let removed_panel = self.panels.iter().find(|p| p.id == id).cloned().unwrap();
self.panels.retain(|e| e.id != id);
let mut buffer = removed_panel.size;
for panel in &mut self.panels.iter_mut() {
let resized_sized = (panel.initial_size - panel.size).min(buffer);
panel.size = (panel.size + resized_sized).max(panel.min_size);
let new_resized_sized = panel.initial_size - panel.size;
buffer -= new_resized_sized;
}
}
pub fn apply_resize(&mut self, panel_index: usize, distance: f32) -> bool {
let mut changed_panels = false;
let (corrected_distance, behind_range, forward_range) = if distance >= 0. {
(distance, 0..panel_index, panel_index..self.panels.len())
} else {
(-distance, panel_index..self.panels.len(), 0..panel_index)
};
let mut acc_per = 0.0;
for panel in &mut self.panels[forward_range].iter_mut() {
let old_size = panel.size;
let new_size = (panel.size - corrected_distance).clamp(panel.min_size, 100.);
if panel.size != new_size {
changed_panels = true
}
panel.size = new_size;
acc_per -= new_size - old_size;
if old_size > panel.min_size {
break;
}
}
if let Some(panel) = &mut self.panels[behind_range].iter_mut().next_back() {
let new_size = (panel.size + acc_per).clamp(panel.min_size, 100.);
if panel.size != new_size {
changed_panels = true
}
panel.size = new_size;
}
changed_panels
}
}
#[component]
pub fn ResizableContainer(
#[props(default = "vertical".to_string())]
direction: String,
children: Element,
) -> Element {
let (node_reference, size) = use_node_signal();
use_context_provider(|| size);
use_context_provider(|| {
Signal::new(ResizableContext {
direction: direction.clone(),
..Default::default()
})
});
rsx!(
rect {
reference: node_reference,
direction,
width: "fill",
height: "fill",
content: "flex",
{children}
}
)
}
#[component]
pub fn ResizablePanel(
#[props(default = 50.)]
initial_size: f32,
min_size: Option<f32>,
children: Element,
order: Option<usize>,
) -> Element {
let mut registry = use_context::<Signal<ResizableContext>>();
let id = use_hook(move || {
let id = UseId::<ResizableContext>::get_in_hook();
let panel = Panel {
initial_size,
size: initial_size,
min_size: min_size.unwrap_or(initial_size * 0.25),
id,
};
registry.write().push_panel(panel, order);
id
});
use_drop(move || {
registry.write().remove_panel(id);
});
let registry = registry.read();
let index = registry
.panels
.iter()
.position(|e| e.id == id)
.unwrap_or_default();
let Panel { size, .. } = registry.panels[index];
let (width, height) = match registry.direction.as_str() {
"horizontal" => (format!("flex({size})"), "fill".to_owned()),
_ => ("fill".to_owned(), format!("flex({size}")),
};
rsx!(
if index > 0 {
ResizableHandle {
panel_index: index
}
}
rect {
width: "{width}",
height: "{height}",
overflow: "clip",
{children}
}
)
}
#[derive(Debug, Default, PartialEq, Clone, Copy)]
pub enum HandleStatus {
#[default]
Idle,
Hovering,
}
#[component]
fn ResizableHandle(
panel_index: usize,
theme: Option<ResizableHandleThemeWith>,
) -> Element {
let ResizableHandleTheme {
background,
hover_background,
} = use_applied_theme!(&theme, resizable_handle);
let (node_reference, size) = use_node_signal();
let mut clicking = use_signal(|| false);
let mut status = use_signal(HandleStatus::default);
let mut registry = use_context::<Signal<ResizableContext>>();
let container_size = use_context::<ReadOnlySignal<NodeReferenceLayout>>();
let platform = use_platform();
let mut allow_resizing = use_signal(|| false);
use_effect(move || {
size.read();
allow_resizing.set(true);
});
use_drop(move || {
if *status.peek() == HandleStatus::Hovering {
platform.set_cursor(CursorIcon::default());
}
});
let cursor = match registry.read().direction.as_str() {
"horizontal" => CursorIcon::ColResize,
_ => CursorIcon::RowResize,
};
let onpointerleave = move |_: PointerEvent| {
*status.write() = HandleStatus::Idle;
if !clicking() {
platform.set_cursor(CursorIcon::default());
}
};
let onpointerenter = move |e: PointerEvent| {
e.stop_propagation();
*status.write() = HandleStatus::Hovering;
platform.set_cursor(cursor);
};
let oncaptureglobalmousemove = move |e: MouseEvent| {
if clicking() {
if !allow_resizing() {
return;
}
let coordinates = e.get_screen_coordinates();
let mut registry = registry.write();
let total_size = registry.panels.iter().fold(0., |acc, p| acc + p.size);
let distance = match registry.direction.as_str() {
"horizontal" => {
let container_width = container_size.read().area.width();
let displacement = coordinates.x as f32 - size.read().area.min_x();
total_size / container_width * displacement
}
_ => {
let container_height = container_size.read().area.height();
let displacement = coordinates.y as f32 - size.read().area.min_y();
total_size / container_height * displacement
}
};
let changed_panels = registry.apply_resize(panel_index, distance);
if changed_panels {
allow_resizing.set(false);
}
e.prevent_default();
}
};
let onpointerdown = move |e: PointerEvent| {
e.stop_propagation();
e.prevent_default();
clicking.set(true);
};
let onglobalpointerup = move |_| {
if clicking() {
if *status.peek() != HandleStatus::Hovering {
platform.set_cursor(CursorIcon::default());
}
clicking.set(false);
}
};
let (width, height) = match registry.read().direction.as_str() {
"horizontal" => ("4", "fill"),
_ => ("fill", "4"),
};
let background = match status() {
_ if clicking() => hover_background,
HandleStatus::Hovering => hover_background,
HandleStatus::Idle => background,
};
rsx!(rect {
reference: node_reference,
width: "{width}",
height: "{height}",
background: "{background}",
onpointerdown,
onglobalpointerup,
onpointerenter,
oncaptureglobalmousemove,
onpointerleave,
})
}
#[cfg(test)]
mod test {
use freya::prelude::*;
use freya_testing::prelude::*;
#[tokio::test]
pub async fn resizable_container() {
fn resizable_container_app() -> Element {
rsx!(
ResizableContainer {
ResizablePanel {
min_size: 4.,
label {
"Panel 0"
}
}
ResizablePanel {
min_size: 4.,
ResizableContainer {
direction: "horizontal",
ResizablePanel {
min_size: 4.,
label {
"Panel 2"
}
}
ResizablePanel {
min_size: 4.,
label {
"Panel 3"
}
}
ResizablePanel {
min_size: 4.,
label {
"Panel 4"
}
}
}
}
}
)
}
let mut utils = launch_test(resizable_container_app);
utils.wait_for_update().await;
let root = utils.root();
let container = root.get(0);
let panel_0 = container.get(1);
let panel_1 = container.get(3);
let panel_2 = panel_1.get(0).get(1);
let panel_3 = panel_1.get(0).get(3);
let panel_4 = panel_1.get(0).get(5);
assert_eq!(panel_0.layout().unwrap().area.height().round(), 248.0);
assert_eq!(panel_1.layout().unwrap().area.height().round(), 248.0);
assert_eq!(panel_2.layout().unwrap().area.width().round(), 164.0);
assert_eq!(panel_3.layout().unwrap().area.width().round(), 164.0);
assert_eq!(panel_4.layout().unwrap().area.width().round(), 164.0);
utils.push_event(TestEvent::Mouse {
name: MouseEventName::MouseDown,
cursor: (100.0, 250.0).into(),
button: Some(MouseButton::Left),
});
utils.wait_for_update().await;
utils.push_event(TestEvent::Mouse {
name: MouseEventName::MouseMove,
cursor: (100.0, 200.0).into(),
button: Some(MouseButton::Left),
});
utils.wait_for_update().await;
utils.push_event(TestEvent::Mouse {
name: MouseEventName::MouseUp,
cursor: (0.0, 0.0).into(),
button: Some(MouseButton::Left),
});
utils.wait_for_update().await;
assert_eq!(panel_0.layout().unwrap().area.height().round(), 200.0); assert_eq!(panel_1.layout().unwrap().area.height().round(), 296.0); utils.push_event(TestEvent::Mouse {
name: MouseEventName::MouseDown,
cursor: (167.0, 300.0).into(),
button: Some(MouseButton::Left),
});
utils.wait_for_update().await;
utils.push_event(TestEvent::Mouse {
name: MouseEventName::MouseMove,
cursor: (187.0, 300.0).into(),
button: Some(MouseButton::Left),
});
utils.wait_for_update().await;
utils.push_event(TestEvent::Mouse {
name: MouseEventName::MouseUp,
cursor: (0.0, 0.0).into(),
button: Some(MouseButton::Left),
});
utils.wait_for_update().await;
utils.wait_for_update().await;
utils.wait_for_update().await;
assert_eq!(panel_2.layout().unwrap().area.width().round(), 187.0); assert_eq!(panel_3.layout().unwrap().area.width().round(), 141.0);
}
}