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;
enum ResizableItem {
Panel(f32),
Handle,
}
impl ResizableItem {
fn size(&self) -> f32 {
match self {
Self::Panel(size) => *size,
Self::Handle => panic!("Not a Panel"),
}
}
fn try_write_size(&mut self) -> Option<&mut f32> {
match self {
Self::Panel(old_size) => Some(old_size),
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",
{children}
}
)
}
#[component]
pub fn ResizablePanel(
#[props(default = 10.)]
initial_size: f32, children: Element,
) -> Element {
let mut registry = use_context::<Signal<ResizableContext>>();
let index = use_hook(move || {
registry
.write()
.registry
.push(ResizableItem::Panel(initial_size));
registry.peek().registry.len() - 1
});
let registry = registry.read();
let is_last = registry.registry.len() - 1 == index;
let size = registry.registry[index].size();
let extra_gap = if is_last { 0 } else { 4 };
let (width, height) = match registry.direction.as_str() {
"horizontal" => (format!("calc({size}% - {})", extra_gap), "fill".to_owned()),
_ => ("fill".to_owned(), format!("calc({size}% - {})", extra_gap)),
};
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();
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.peek() {
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.peek() {
let coordinates = e.get_screen_coordinates();
let mut registry = registry.write();
let displacement_per = match registry.direction.as_str() {
"horizontal" => {
let container_width = container_size.read().area.width();
let displacement = coordinates.x as f32 - size.peek().area.min_x();
100. / container_width * displacement
}
_ => {
let container_height = container_size.read().area.height();
let displacement = coordinates.y as f32 - size.peek().area.min_y();
100. / container_height * displacement
}
};
if displacement_per > 0. {
let mut acc_per = 0.0;
for next_item in &mut registry.registry[index..].iter_mut() {
if let Some(size) = next_item.try_write_size() {
let old_size = *size;
let new_size = (*size - displacement_per).clamp(4., 100.);
*size = new_size;
acc_per -= new_size - old_size;
break;
}
}
for prev_item in &mut registry.registry[0..index].iter_mut().rev() {
if let Some(size) = prev_item.try_write_size() {
let new_size = (*size + acc_per).clamp(4., 100.);
*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(size) = prev_item.try_write_size() {
let old_size = *size;
let new_size = (*size + displacement_per).clamp(4., 100.);
*size = new_size;
acc_per += new_size - old_size;
break;
}
}
for next_item in &mut registry.registry[index..].iter_mut() {
if let Some(size) = next_item.try_write_size() {
let new_size = (*size - acc_per).clamp(4., 100.);
*size = new_size;
break;
}
}
}
}
};
let onmousedown = move |e: MouseEvent| {
e.stop_propagation();
clicking.set(true);
};
let onclick = move |_: MouseEvent| {
if *clicking.peek() {
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(), 250.0);
assert_eq!(panel_1.layout().unwrap().area.height().round(), 250.0);
assert_eq!(panel_2.layout().unwrap().area.width().round(), 167.0);
assert_eq!(panel_3.layout().unwrap().area.width().round(), 167.0);
assert_eq!(panel_4.layout().unwrap().area.width().round(), 167.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(), 300.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: (185.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(), 185.0); assert_eq!(panel_3.layout().unwrap().area.width().round(), 148.0);
}
}