1#![doc = include_str!("../README.md")]
2
3use leptos::{ev, html::Div, prelude::*, text_prop::TextProp};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7pub enum SplitDirection {
8 #[default]
10 Row,
11 Column,
13}
14
15#[component]
35pub fn ResizableSplit(
36 #[prop(into, optional)]
38 direction: SplitDirection,
39 #[prop(into, optional)]
41 class: TextProp,
42 #[prop(into, optional)]
44 handle_class: TextProp,
45 #[prop(optional)]
47 percentages: Option<RwSignal<Vec<f64>>>,
48 children: ChildrenFragment,
49) -> impl IntoView {
50 let container = NodeRef::<Div>::new();
51 let (is_resizing, set_resizing) = signal(false);
52 let (resizing_index, set_resizing_index) = signal(None::<usize>);
53
54 let children = children().nodes.into_iter().collect::<Vec<_>>();
56 let num_children = children.len();
57
58 let percentages = percentages.unwrap_or_else(|| {
60 let initial_size = 100.0 / num_children as f64;
61 RwSignal::new(vec![initial_size; num_children - 1])
62 });
63
64 let on_mouse_down = move |index: usize| {
65 move |ev: ev::MouseEvent| {
66 ev.prevent_default();
67 set_resizing.set(true);
68 set_resizing_index.set(Some(index));
69 }
70 };
71
72 let on_mouse_move = move |ev: ev::MouseEvent| {
73 ev.prevent_default();
74
75 if !is_resizing.get() {
76 return;
77 }
78
79 if let (Some(index), Some(container)) =
80 (resizing_index.get(), container.get())
81 {
82 let bounds = container.get_bounding_client_rect();
83
84 let new_percentages = match direction {
85 SplitDirection::Row => {
86 let x = ev.client_x() as f64 - bounds.left();
87 let x_percentage = (x / bounds.width()) * 100.0;
88 calculate_new_percentages(percentages.get(), index, x_percentage)
89 }
90 SplitDirection::Column => {
91 let y = ev.client_y() as f64 - bounds.top();
92 let y_percentage = (y / bounds.height()) * 100.0;
93 calculate_new_percentages(percentages.get(), index, y_percentage)
94 }
95 };
96 percentages.set(new_percentages);
97 }
98 };
99
100 let stop_resize = move |ev: ev::MouseEvent| {
101 ev.prevent_default();
102 set_resizing.set(false);
103 set_resizing_index.set(None);
104 };
105
106 let grid_template_columns = move || match direction {
107 SplitDirection::Row => {
108 let mut grid_template = String::new();
109 for (i, percentage) in percentages.get().iter().enumerate() {
110 grid_template.push_str(&format!("{:.2}% ", percentage));
111 if i < percentages.get().len() - 1 {
112 grid_template.push(' ');
113 }
114 }
115 grid_template.push_str(&format!(
116 "{:.2}%",
117 100.0 - percentages.get().iter().sum::<f64>()
118 ));
119 grid_template
120 }
121 SplitDirection::Column => "100%".into(),
122 };
123
124 let grid_template_rows = move || match direction {
125 SplitDirection::Row => "100%".into(),
126 SplitDirection::Column => {
127 let mut grid_template = String::new();
128 for (i, percentage) in percentages.get().iter().enumerate() {
129 grid_template.push_str(&format!("{:.2}% ", percentage));
130 if i < percentages.get().len() - 1 {
131 grid_template.push(' ');
132 }
133 }
134 grid_template.push_str(&format!(
135 "{:.2}%",
136 100.0 - percentages.get().iter().sum::<f64>()
137 ));
138 grid_template
139 }
140 };
141
142 let handle_style = move |index: usize| {
143 let position = percentages
144 .get()
145 .iter()
146 .take(index + 1)
147 .fold(0.0, |acc, &x| acc + x);
148
149 let styles = match direction {
150 SplitDirection::Row => {
151 format!(
152 "cursor: col-resize; width: 20px; transform: translateX(-50%, 0%); \
153 top: 0; bottom: 0; left: calc({:.2}% - 10px)",
154 position
155 )
156 }
157 SplitDirection::Column => {
158 format!(
159 "cursor: row-resize; height: 20px; transform: translateY(-50%, 0%); \
160 top: calc({:.2}% - 10px); left: 0; right: 0",
161 position
162 )
163 }
164 };
165 format!("position: absolute; z-index: 1; {}", styles)
166 };
167
168 view! {
169 <div
170 node_ref=container
171 class=move || class.get()
172 style:position="relative"
173 on:mousemove=on_mouse_move
174 on:mouseleave=stop_resize
175 >
176 {(0..num_children - 1)
177 .map({
178 let handle_class = handle_class.clone();
179 move |index| {
180 view! {
181 <div
182 class={
183 let handle_class = handle_class.clone();
184 move || handle_class.get()
185 }
186 style=move || handle_style(index)
187 on:mousedown=on_mouse_down(index)
188 on:mouseup=stop_resize
189 />
190 }
191 }
192 })
193 .collect_view()}
194 <div
195 style:display="grid"
196 style:grid-template-columns=grid_template_columns
197 style:grid-template-rows=grid_template_rows
198 style:width="100%"
199 style:height="100%"
200 >
201 {children}
202 </div>
203 </div>
204 }
205}
206
207fn calculate_new_percentages(
210 current_percentages: Vec<f64>,
211 index: usize,
212 new_percentage: f64,
213) -> Vec<f64> {
214 let mut new_percentages = current_percentages.clone();
215 if index < new_percentages.len() {
216 let prev_sum: f64 = current_percentages.iter().take(index).sum();
217 let next_sum: f64 = current_percentages.iter().skip(index + 1).sum();
218 let current_percentage = new_percentages[index];
219
220 if new_percentage >= prev_sum && new_percentage <= 100.0 - next_sum {
222 new_percentages[index] = new_percentage - prev_sum;
223 if index < new_percentages.len() - 1 {
224 new_percentages[index + 1] +=
225 current_percentage - new_percentages[index];
226 }
227 }
228 }
229 new_percentages
230}