use dioxus::prelude::*;
use freya_common::NodeReferenceLayout;
use freya_elements::{
elements as dioxus_elements,
events::MouseEvent,
};
use freya_hooks::{
use_applied_theme,
use_node_signal,
use_platform,
ResizableHandleTheme,
ResizableHandleThemeWith,
};
use winit::window::CursorIcon;
struct Panel {
pub size: f32,
pub min_size: f32,
}
enum ResizableItem {
Panel(Panel),
Handle,
}
impl ResizableItem {
fn panel(&self) -> &Panel {
match self {
Self::Panel(panel) => panel,
Self::Handle => panic!("Not a Panel"),
}
}
fn try_panel_mut(&mut self) -> Option<&mut Panel> {
match self {
Self::Panel(panel) => Some(panel),
Self::Handle => None,
}
}
}
#[derive(Default)]
struct ResizableContext {
pub registry: Vec<ResizableItem>,
pub direction: String,
}
#[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: "{direction}",
width: "fill",
height: "fill",
content: "flex",
{children}
}
)
}
#[component]
pub fn ResizablePanel(
#[props(default = 10.)]
initial_size: f32, #[props(default = 4.)]
min_size: f32,
children: Element,
) -> Element {
let mut registry = use_context::<Signal<ResizableContext>>();
let index = use_hook(move || {
registry.write().registry.push(ResizableItem::Panel(Panel {
size: initial_size,
min_size,
}));
registry.peek().registry.len() - 1
});
let registry = registry.read();
let Panel { size, .. } = registry.registry[index].panel();
let (width, height) = match registry.direction.as_str() {
"horizontal" => (format!("flex({size})"), "fill".to_owned()),
_ => ("fill".to_owned(), format!("flex({size}")),
};
rsx!(
rect {
width: "{width}",
height: "{height}",
overflow: "clip",
{children}
}
)
}
#[derive(Debug, Default, PartialEq, Clone, Copy)]
pub enum HandleStatus {
#[default]
Idle,
Hovering,
}
#[component]
pub fn ResizableHandle(
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_memo(move || {
size.read();
allow_resizing.set(true);
});
use_drop(move || {
if *status.peek() == HandleStatus::Hovering {
platform.set_cursor(CursorIcon::default());
}
});
let index = use_hook(move || {
registry.write().registry.push(ResizableItem::Handle);
registry.peek().registry.len() - 1
});
let cursor = match registry.read().direction.as_str() {
"horizontal" => CursorIcon::ColResize,
_ => CursorIcon::RowResize,
};
let onmouseleave = move |_: MouseEvent| {
*status.write() = HandleStatus::Idle;
if !clicking() {
platform.set_cursor(CursorIcon::default());
}
};
let onmouseenter = move |e: MouseEvent| {
e.stop_propagation();
*status.write() = HandleStatus::Hovering;
platform.set_cursor(cursor);
};
let onmousemove = move |e: MouseEvent| {
if clicking() {
if !allow_resizing() {
return;
}
let coordinates = e.get_screen_coordinates();
let mut registry = registry.write();
let displacement_per: f32 = 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();
100. / container_width * displacement
}
_ => {
let container_height = container_size.read().area.height();
let displacement = coordinates.y as f32 - size.read().area.min_y();
100. / container_height * displacement
}
};
let mut changed_panels = false;
if displacement_per >= 0. {
let mut acc_per = 0.0;
for next_item in &mut registry.registry[index..].iter_mut() {
if let Some(panel) = next_item.try_panel_mut() {
let old_size = panel.size;
let new_size = (panel.size - displacement_per).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;
}
}
}
for prev_item in &mut registry.registry[0..index].iter_mut().rev() {
if let Some(panel) = prev_item.try_panel_mut() {
let new_size = (panel.size + acc_per).clamp(panel.min_size, 100.);
if panel.size != new_size {
changed_panels = true
}
panel.size = new_size;
break;
}
}
} else {
let mut acc_per = 0.0;
for prev_item in &mut registry.registry[0..index].iter_mut().rev() {
if let Some(panel) = prev_item.try_panel_mut() {
let old_size = panel.size;
let new_size = (panel.size + displacement_per).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;
}
}
}
for next_item in &mut registry.registry[index..].iter_mut() {
if let Some(panel) = next_item.try_panel_mut() {
let new_size = (panel.size - acc_per).clamp(panel.min_size, 100.);
if panel.size != new_size {
changed_panels = true
}
panel.size = new_size;
break;
}
}
}
if changed_panels {
allow_resizing.set(false);
}
}
};
let onmousedown = move |e: MouseEvent| {
e.stop_propagation();
clicking.set(true);
};
let onclick = move |_: MouseEvent| {
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}",
onmousedown,
onglobalclick: onclick,
onmouseenter,
onglobalmousemove: onmousemove,
onmouseleave,
})
}
#[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 {
initial_size: 50.,
label {
"Panel 0"
}
}
ResizableHandle { }
ResizablePanel { initial_size: 50.,
ResizableContainer {
direction: "horizontal",
ResizablePanel {
initial_size: 33.33,
label {
"Panel 2"
}
}
ResizableHandle { }
ResizablePanel {
initial_size: 33.33,
label {
"Panel 3"
}
}
ResizableHandle { }
ResizablePanel {
initial_size: 33.33,
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(0);
let panel_1 = container.get(2);
let panel_2 = panel_1.get(0).get(0);
let panel_3 = panel_1.get(0).get(2);
let panel_4 = panel_1.get(0).get(4);
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(PlatformEvent::Mouse {
name: EventName::MouseDown,
cursor: (100.0, 250.0).into(),
button: Some(MouseButton::Left),
});
utils.push_event(PlatformEvent::Mouse {
name: EventName::MouseMove,
cursor: (100.0, 200.0).into(),
button: Some(MouseButton::Left),
});
utils.push_event(PlatformEvent::Mouse {
name: EventName::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(PlatformEvent::Mouse {
name: EventName::MouseDown,
cursor: (167.0, 300.0).into(),
button: Some(MouseButton::Left),
});
utils.push_event(PlatformEvent::Mouse {
name: EventName::MouseMove,
cursor: (187.0, 300.0).into(),
button: Some(MouseButton::Left),
});
utils.push_event(PlatformEvent::Mouse {
name: EventName::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);
}
}