1#![doc = include_str!("../README.md")]
2use std::fmt::Display;
3
4use regex::Regex;
5use wasm_bindgen::{__rt::IntoJsResult, prelude::*};
6use web_sys::{Element, HtmlElement};
7use yew::prelude::*;
8
9mod js;
10
11#[derive(Clone, Properties, PartialEq)]
12pub struct SplitProps {
13 #[prop_or_default]
15 pub class: Classes,
16
17 #[prop_or_default]
19 pub sizes: Option<Vec<f64>>,
20
21 #[prop_or_default]
23 pub min_size: Option<f64>,
24
25 #[prop_or_default]
27 pub min_sizes: Option<Vec<f64>>,
28
29 #[prop_or_default]
31 pub max_size: Option<f64>,
32
33 #[prop_or_default]
35 pub max_sizes: Option<Vec<f64>>,
36
37 #[prop_or_default]
39 pub expand_to_min: Option<bool>,
40
41 #[prop_or_default]
43 pub gutter_size: Option<f64>,
44
45 #[prop_or_default]
47 pub gutter_align: Option<GutterAlign>,
48
49 #[prop_or_default]
51 pub snap_offset: Option<f64>,
52
53 #[prop_or_default]
55 pub drag_interval: Option<f64>,
56
57 #[prop_or_default]
59 pub direction: Option<Direction>,
60
61 #[prop_or_default]
63 pub cursor: Option<Cursor>,
64
65 #[prop_or(
67 Closure::<dyn Fn(js_sys::BigInt, String, Element) -> Element>::new(
68 |_index, direction, _pair_element| {
69 let gutter_element = web_sys::window()
70 .expect_throw("No window")
71 .document()
72 .expect_throw("No document")
73 .create_element("div")
74 .expect_throw("Failed to create gutter div");
75
76 gutter_element.set_class_name(&format!("gutter gutter-{}", direction));
77 js_sys::Reflect::set(
78 &gutter_element,
79 &"__isSplitGutter".into(),
80 &true.into(),
81 )
82 .expect_throw("Unable to set __isSplitGutter property");
83 gutter_element
84 },
85 )
86 .into_js_value()
87 .into()
88 )]
89 pub gutter: js_sys::Function,
90
91 #[prop_or_default]
93 pub element_style: Option<js_sys::Function>,
94
95 #[prop_or_default]
97 pub gutter_style: Option<js_sys::Function>,
98
99 #[prop_or_default]
101 pub on_drag: Option<js_sys::Function>,
102
103 #[prop_or_default]
105 pub on_drag_start: Option<js_sys::Function>,
106
107 #[prop_or_default]
109 pub on_drag_end: Option<js_sys::Function>,
110
111 #[prop_or_default]
112 pub collapsed: Option<usize>,
113
114 pub children: Children,
115}
116
117pub struct Split {
118 parent_ref: NodeRef,
119 split: Option<js::Split>,
120}
121
122impl Component for Split {
123 type Message = ();
124 type Properties = SplitProps;
125
126 fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
127 if first_render {
128 let children = self
129 .parent_ref
130 .cast::<HtmlElement>()
131 .unwrap_throw()
132 .children();
133
134 let children = js_sys::Array::from(&children);
135 let options = ctx.props().make_options_object();
136 self.split = Some(js::Split::new(children, options));
137
138 if let Some(collapsed) = ctx.props().collapsed {
139 self.split.as_ref().unwrap_throw().collapse(collapsed);
140 }
141 }
142 }
143
144 fn create(_ctx: &Context<Self>) -> Self {
145 Self {
146 parent_ref: NodeRef::default(),
147 split: None,
148 }
149 }
150
151 fn view(&self, ctx: &Context<Self>) -> Html {
152 let SplitProps {
153 class, children, ..
154 } = ctx.props();
155
156 html! {
157 <div class={(*class).clone()} ref={self.parent_ref.clone()}>
158 { for children.iter() }
159 </div>
160 }
161 }
162
163 fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
164 let SplitProps {
165 sizes,
166 min_size,
167 min_sizes,
168 collapsed,
169 ..
170 } = ctx.props();
171
172 let SplitProps {
173 min_size: old_min_size,
174 min_sizes: old_min_sizes,
175 sizes: old_sizes,
176 collapsed: old_collapsed,
177 ..
178 } = old_props;
179
180 let mut needs_recreate = ctx.props().other_props_changed(old_props);
181
182 if min_sizes.is_some() && old_min_sizes.is_some() {
183 let mut min_size_changed = false;
184
185 min_sizes
186 .as_ref()
187 .unwrap_throw()
188 .iter()
189 .enumerate()
190 .for_each(|(i, min_size_i)| {
191 min_size_changed |= min_size_i
192 != old_min_sizes.as_ref().unwrap_throw().get(i).expect_throw(
193 "Cannot index min_sizes during update. Did the length change?",
194 );
195 });
196
197 needs_recreate |= min_size_changed;
198 } else if min_sizes.is_some() || old_min_sizes.is_some() {
199 needs_recreate = true;
200 } else {
201 needs_recreate |= min_size != old_min_size;
202 }
203
204 if needs_recreate {
205 let options = ctx.props().make_options_object();
206
207 let cur_sizes = js_sys::Reflect::get(&options, &"sizes".into()).unwrap_throw();
209 if cur_sizes.is_falsy() {
210 js_sys::Reflect::set(
211 &options,
212 &"sizes".into(),
213 &js::Split::get_sizes(self.split.as_ref().unwrap_throw()),
214 )
215 .unwrap_throw();
216 }
217
218 js::Split::destroy(self.split.as_ref().unwrap_throw(), true.into(), true.into());
219
220 let new_gutter: js_sys::Function =
223 Closure::<dyn Fn(js_sys::BigInt, String, Element) -> Element>::new(
224 |_index, direction, pair_element: Element| {
225 let gutter_el: Element = pair_element
226 .previous_sibling()
227 .map(|node| node.into_js_result().unwrap_throw())
228 .unwrap_or(JsValue::UNDEFINED)
229 .into();
230
231 if direction == "horizontal" {
232 gutter_el.set_class_name("gutter gutter-horizontal");
233 } else {
234 gutter_el.set_class_name("gutter gutter-vertical");
235 }
236
237 gutter_el
241 .set_attribute("style", "")
242 .expect_throw("Cannot reset gutter style on recreate");
243
244 gutter_el
245 },
246 )
247 .into_js_value()
248 .into();
249 js_sys::Reflect::set(&options, &"gutter".into(), &new_gutter).unwrap_throw();
250
251 let non_gutter_children = js_sys::Array::from(
252 &self
253 .parent_ref
254 .cast::<HtmlElement>()
255 .expect_throw("No parent during update")
256 .children(),
257 )
258 .filter(&mut |element, _, _| {
259 js_sys::Reflect::get(&element, &"__isSplitGutter".into())
260 .unwrap_throw()
261 .is_falsy()
262 });
263
264 let width_regex = Regex::new(r"width: .*;").unwrap_throw();
267 let height_regex = Regex::new(r"height: .*;").unwrap_throw();
268 for child in non_gutter_children.iter() {
269 let child: Element = child.into();
270 if let Some(style) = child.get_attribute("style") {
271 let new_style = match ctx
272 .props()
273 .direction
274 .as_ref()
275 .expect_throw("No direction during update")
276 {
277 Direction::Vertical => width_regex.replace_all(&style, ""),
280 Direction::Horizontal => height_regex.replace_all(&style, ""),
281 };
282 child
283 .set_attribute("style", &new_style)
284 .expect_throw("Cannot reset child style on recreate");
285 }
286 }
287
288 self.split = Some(js::Split::new(non_gutter_children, options));
289 } else if sizes.is_some() {
290 let mut size_changed = false;
291
292 sizes
293 .as_ref()
294 .unwrap_throw()
295 .iter()
296 .enumerate()
297 .for_each(|(i, size_i)| {
298 size_changed |= size_i
299 != old_sizes.as_ref().unwrap_throw().get(i).expect_throw(
300 "Cannot index sizes during update. Did the length change?",
301 );
302 });
303
304 if size_changed {
305 let new_sizes = js_sys::Array::new();
306 for size in sizes.as_ref().unwrap_throw().iter() {
307 new_sizes.push(&JsValue::from(*size));
308 }
309 js::Split::set_sizes(self.split.as_ref().unwrap_throw(), new_sizes);
310 }
311 }
312
313 if collapsed.is_some()
314 && (old_collapsed.is_some() && collapsed.unwrap_throw() != old_collapsed.unwrap_throw()
315 || needs_recreate)
316 {
317 js::Split::collapse(self.split.as_ref().unwrap_throw(), collapsed.unwrap_throw());
318 }
319
320 true
321 }
322
323 fn destroy(&mut self, _ctx: &Context<Self>) {
324 js::Split::destroy(
325 self.split.as_ref().unwrap_throw(),
326 false.into(),
327 false.into(),
328 );
329 self.split = None;
330 }
331}
332
333#[derive(Debug, Clone, PartialEq)]
334pub enum GutterAlign {
335 Start,
336 End,
337 Center,
338}
339
340impl Display for GutterAlign {
341 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342 match self {
343 GutterAlign::Start => write!(f, "start"),
344 GutterAlign::End => write!(f, "end"),
345 GutterAlign::Center => write!(f, "center"),
346 }
347 }
348}
349
350#[derive(Debug, Clone, PartialEq)]
351pub enum Direction {
352 Vertical,
353 Horizontal,
354}
355
356impl Display for Direction {
357 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
358 match self {
359 Direction::Vertical => write!(f, "vertical"),
360 Direction::Horizontal => write!(f, "horizontal"),
361 }
362 }
363}
364
365#[derive(Debug, Clone, PartialEq)]
366pub enum Cursor {
367 ColResize,
368 RowResize,
369}
370
371impl Display for Cursor {
372 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
373 match self {
374 Cursor::ColResize => write!(f, "col-resize"),
375 Cursor::RowResize => write!(f, "row-resize"),
376 }
377 }
378}
379
380impl SplitProps {
381 fn other_props_changed(&self, old_props: &SplitProps) -> bool {
382 let SplitProps {
383 max_size,
384 expand_to_min,
385 gutter_size,
386 gutter_align,
387 snap_offset,
388 drag_interval,
389 direction,
390 cursor,
391 ..
392 } = self;
393
394 let SplitProps {
395 max_size: old_max_size,
396 expand_to_min: old_expand_to_min,
397 gutter_size: old_gutter_size,
398 gutter_align: old_gutter_align,
399 snap_offset: old_snap_offset,
400 drag_interval: old_drag_interval,
401 direction: old_direction,
402 cursor: old_cursor,
403 ..
404 } = old_props;
405
406 max_size != old_max_size
407 || expand_to_min != old_expand_to_min
408 || gutter_size != old_gutter_size
409 || gutter_align != old_gutter_align
410 || snap_offset != old_snap_offset
411 || drag_interval != old_drag_interval
412 || direction != old_direction
413 || cursor != old_cursor
414 }
415
416 fn set_option<T, F: Fn(&T) -> JsValue>(
417 options: &js_sys::Object,
418 key: &str,
419 value: &Option<T>,
420 f: F,
421 ) {
422 if value.is_some() {
423 js_sys::Reflect::set(
424 options,
425 &key.into(),
426 &value.as_ref().map_or(JsValue::UNDEFINED, f),
427 )
428 .unwrap_throw();
429 }
430 }
431
432 fn make_options_object(&self) -> js_sys::Object {
433 let SplitProps {
434 sizes,
435 min_size,
436 min_sizes,
437 max_size,
438 max_sizes,
439 expand_to_min,
440 gutter_size,
441 gutter_align,
442 snap_offset,
443 drag_interval,
444 direction,
445 cursor,
446 gutter,
447 element_style,
448 gutter_style,
449 on_drag,
450 on_drag_start,
451 on_drag_end,
452 ..
453 } = self;
454
455 let options = js_sys::Object::new();
456 let f64_vec_to_arr = |v: &Vec<f64>| {
457 let arr = js_sys::Array::new();
458 for val in v.iter() {
459 arr.push(&JsValue::from(*val));
460 }
461 arr.into()
462 };
463
464 fn val_to_js<T: Into<JsValue> + Clone>(v: &T) -> JsValue {
465 (*v).clone().into()
466 }
467
468 fn to_js_string<T: Display>(v: &T) -> JsValue {
469 v.to_string().into()
470 }
471
472 Self::set_option(&options, "sizes", sizes, f64_vec_to_arr);
473
474 if min_sizes.is_some() {
475 Self::set_option(&options, "minSize", min_sizes, f64_vec_to_arr);
476 } else if min_size.is_some() {
477 Self::set_option(&options, "minSize", min_size, val_to_js);
478 }
479
480 if max_sizes.is_some() {
481 Self::set_option(&options, "maxSize", max_sizes, f64_vec_to_arr);
482 } else if max_size.is_some() {
483 Self::set_option(&options, "maxSize", max_size, val_to_js);
484 }
485
486 Self::set_option(&options, "expandToMin", expand_to_min, val_to_js);
487 Self::set_option(&options, "gutterSize", gutter_size, val_to_js);
488 Self::set_option(&options, "gutterAlign", gutter_align, to_js_string);
489 Self::set_option(&options, "snapOffset", snap_offset, val_to_js);
490 Self::set_option(&options, "dragInterval", drag_interval, val_to_js);
491 Self::set_option(&options, "direction", direction, to_js_string);
492 Self::set_option(&options, "cursor", cursor, to_js_string);
493 Self::set_option(&options, "gutter", &Some(gutter), val_to_js);
494 Self::set_option(&options, "elementStyle", element_style, val_to_js);
495 Self::set_option(&options, "gutterStyle", gutter_style, val_to_js);
496 Self::set_option(&options, "onDrag", on_drag, val_to_js);
497 Self::set_option(&options, "onDragStart", on_drag_start, val_to_js);
498 Self::set_option(&options, "onDragEnd", on_drag_end, val_to_js);
499
500 options
501 }
502}