1mod layout;
2mod node;
3mod reconcile;
4
5pub(crate) use layout::measure_splitter;
6pub(crate) use node::SplitterNode;
7pub(crate) use reconcile::{SplitterReconcile, reconcile_splitter};
8
9use std::sync::Arc;
10
11use crate::callback::Callback;
12use crate::core::element::{Element, ElementKind};
13use crate::style::{Length, Style};
14use crate::widgets::Orientation;
15
16#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
23pub enum SplitterHandleMode {
24 #[default]
26 Gutter,
27 Border,
36}
37
38#[derive(Clone, Debug)]
40pub struct SplitterResizeEvent {
41 pub split_id: Option<Arc<str>>,
43 pub weights: Vec<f32>,
45}
46
47#[derive(Clone)]
49pub struct Splitter {
50 pub(crate) orientation: Orientation,
51 pub(crate) children: Vec<Element>,
52 pub(crate) weights: Vec<f32>,
53 pub(crate) weights_nonce: u32,
54 pub(crate) split_id: Option<Arc<str>>,
55 pub(crate) on_resize_live: Option<Callback<SplitterResizeEvent>>,
56 pub(crate) on_resize: Option<Callback<SplitterResizeEvent>>,
57 pub(crate) min_size: u16,
58 pub(crate) handle_size: u16,
59 pub(crate) handle_mode: SplitterHandleMode,
60 pub(crate) handle_symbol: char,
61 pub(crate) handle_style: Style,
62 pub(crate) handle_hover_style: Style,
63 pub(crate) handle_active_style: Style,
64 pub(crate) width: Length,
65 pub(crate) height: Length,
66}
67
68impl Splitter {
69 pub fn new(orientation: Orientation) -> Self {
71 match orientation {
72 Orientation::Horizontal => Self::horizontal(),
73 Orientation::Vertical => Self::vertical(),
74 }
75 }
76
77 pub fn horizontal() -> Self {
79 Self {
80 orientation: Orientation::Horizontal,
81 children: Vec::new(),
82 weights: Vec::new(),
83 weights_nonce: 0,
84 split_id: None,
85 on_resize_live: None,
86 on_resize: None,
87 min_size: 3,
88 handle_size: 1,
89 handle_mode: SplitterHandleMode::Gutter,
90 handle_symbol: '─',
91 handle_style: Style::default(),
92 handle_hover_style: Style::default(),
93 handle_active_style: Style::default(),
94 width: Length::Flex(1),
95 height: Length::Flex(1),
96 }
97 }
98
99 pub fn vertical() -> Self {
101 Self {
102 orientation: Orientation::Vertical,
103 children: Vec::new(),
104 weights: Vec::new(),
105 weights_nonce: 0,
106 split_id: None,
107 on_resize_live: None,
108 on_resize: None,
109 min_size: 3,
110 handle_size: 1,
111 handle_mode: SplitterHandleMode::Gutter,
112 handle_symbol: '│',
113 handle_style: Style::default(),
114 handle_hover_style: Style::default(),
115 handle_active_style: Style::default(),
116 width: Length::Flex(1),
117 height: Length::Flex(1),
118 }
119 }
120
121 pub fn child(mut self, child: impl Into<Element>) -> Self {
123 self.children.push(child.into());
124 self
125 }
126
127 pub fn orientation(mut self, orientation: Orientation) -> Self {
129 if self.orientation != orientation {
130 self.orientation = orientation;
131 self.handle_symbol = match orientation {
132 Orientation::Horizontal => '─',
133 Orientation::Vertical => '│',
134 };
135 }
136 self
137 }
138
139 pub fn children<I>(mut self, children: I) -> Self
142 where
143 I: IntoIterator<Item = Element>,
144 {
145 self.children = children.into_iter().collect();
146 self
147 }
148
149 pub fn weights(mut self, weights: impl Into<Vec<f32>>) -> Self {
151 self.weights = weights.into();
152 self
153 }
154
155 pub fn weights_nonce(mut self, nonce: u32) -> Self {
157 self.weights_nonce = nonce;
158 self
159 }
160
161 pub fn split_id(mut self, id: impl Into<Arc<str>>) -> Self {
163 self.split_id = Some(id.into());
164 self
165 }
166
167 pub fn on_resize_live(mut self, cb: Callback<SplitterResizeEvent>) -> Self {
169 self.on_resize_live = Some(cb);
170 self
171 }
172
173 pub fn on_resize(mut self, cb: Callback<SplitterResizeEvent>) -> Self {
175 self.on_resize = Some(cb);
176 self
177 }
178
179 pub fn min_size(mut self, min_size: u16) -> Self {
181 self.min_size = min_size;
182 self
183 }
184
185 pub fn handle_size(mut self, size: u16) -> Self {
187 self.handle_size = size.max(1);
188 self
189 }
190
191 pub fn handle_mode(mut self, mode: SplitterHandleMode) -> Self {
200 self.handle_mode = mode;
201 self
202 }
203
204 pub fn handle_symbol(mut self, symbol: char) -> Self {
206 self.handle_symbol = symbol;
207 self
208 }
209
210 pub fn handle_style(mut self, style: Style) -> Self {
212 self.handle_style = style;
213 self
214 }
215
216 pub fn handle_hover_style(mut self, style: Style) -> Self {
218 self.handle_hover_style = style;
219 self
220 }
221
222 pub fn handle_active_style(mut self, style: Style) -> Self {
224 self.handle_active_style = style;
225 self
226 }
227
228 pub fn width(mut self, width: Length) -> Self {
230 self.width = width;
231 self
232 }
233
234 pub fn height(mut self, height: Length) -> Self {
236 self.height = height;
237 self
238 }
239}
240
241impl From<Splitter> for Element {
242 fn from(value: Splitter) -> Self {
243 Element::new(ElementKind::Splitter(value))
244 }
245}
246
247impl crate::layout::hash::LayoutHash for Splitter {
248 fn layout_hash(
249 &self,
250 hasher: &mut impl std::hash::Hasher,
251 recurse: &dyn Fn(&Element) -> Option<u64>,
252 ) -> Option<()> {
253 use std::hash::Hash;
254 self.width.hash(hasher);
255 self.height.hash(hasher);
256 self.orientation.hash(hasher);
257 self.min_size.hash(hasher);
258 self.handle_size.hash(hasher);
259 self.handle_mode.hash(hasher);
260 self.handle_symbol.hash(hasher);
261 self.weights.len().hash(hasher);
262 for weight in &self.weights {
263 weight.to_bits().hash(hasher);
264 }
265 self.weights_nonce.hash(hasher);
266
267 let needs_content =
268 matches!(self.width, Length::Auto) || matches!(self.height, Length::Auto);
269 if needs_content {
270 crate::layout::hash::hash_children(&self.children, hasher, recurse)?;
271 }
272 Some(())
273 }
274}
275
276impl Default for Splitter {
277 fn default() -> Self {
278 Self::horizontal()
279 }
280}
281
282pub(crate) fn resolve_weights(explicit: &[f32], previous: &[f32], len: usize) -> Vec<f32> {
283 let mut weights = if previous.len() == len && !previous.is_empty() {
284 previous.to_vec()
285 } else if explicit.len() == len && !explicit.is_empty() {
286 explicit.to_vec()
287 } else {
288 vec![1.0; len]
289 };
290
291 for weight in &mut weights {
292 if *weight < 0.0 {
293 *weight = 0.0;
294 }
295 }
296
297 let sum: f32 = weights.iter().sum();
298 if sum <= f32::EPSILON {
299 return vec![1.0; len];
300 }
301
302 for weight in &mut weights {
303 *weight /= sum;
304 }
305
306 weights
307}
308
309pub(crate) fn sizes_from_weights(weights: &[f32], available: u16, min_size: u16) -> Vec<u16> {
310 let count = weights.len();
311 if count == 0 {
312 return Vec::new();
313 }
314 if available == 0 {
315 return vec![0; count];
316 }
317
318 let total_weight: f32 = weights.iter().sum();
319 let total_weight = if total_weight <= f32::EPSILON {
320 count as f32
321 } else {
322 total_weight
323 };
324
325 let mut sizes = Vec::with_capacity(count);
326 let mut fractions = Vec::with_capacity(count);
327 for weight in weights {
328 let exact = (available as f32) * (*weight / total_weight);
329 let floored = exact.floor();
330 sizes.push(floored as u16);
331 fractions.push(exact - floored);
332 }
333
334 let mut order: Vec<usize> = (0..count).collect();
344 order.sort_by(|a, b| {
345 fractions[*b]
346 .partial_cmp(&fractions[*a])
347 .unwrap_or(std::cmp::Ordering::Equal)
348 .then(a.cmp(b))
349 });
350
351 let used: u16 = sizes.iter().sum();
352 let mut remaining = available.saturating_sub(used) as usize;
353 let mut idx = 0usize;
354 while remaining > 0 {
355 let target = order[idx % count];
356 sizes[target] = sizes[target].saturating_add(1);
357 remaining -= 1;
358 idx += 1;
359 }
360
361 if min_size == 0 {
362 return sizes;
363 }
364
365 let required = min_size.saturating_mul(count as u16);
366 if available < required {
367 return sizes;
368 }
369
370 loop {
371 let mut updated = false;
372 for idx in 0..count {
373 if sizes[idx] < min_size {
374 let deficit = min_size - sizes[idx];
375 sizes[idx] = min_size;
376 let mut remaining = deficit;
377 for size in sizes.iter_mut().take(count) {
378 if remaining == 0 {
379 break;
380 }
381 if *size > min_size {
382 let take = (*size - min_size).min(remaining);
383 *size = size.saturating_sub(take);
384 remaining = remaining.saturating_sub(take);
385 }
386 }
387 updated = true;
388 break;
389 }
390 }
391 if !updated {
392 break;
393 }
394 }
395
396 sizes
397}
398
399pub(crate) fn sizes_to_weights(sizes: &[u16]) -> Vec<f32> {
400 let total: u16 = sizes.iter().sum();
401 if total == 0 {
402 return vec![1.0; sizes.len()];
403 }
404 sizes
405 .iter()
406 .map(|size| (*size as f32) / (total as f32))
407 .collect()
408}
409
410#[cfg(test)]
411mod size_tests {
412 use super::{sizes_from_weights, sizes_to_weights};
413
414 #[test]
418 fn sizes_survive_a_round_trip_through_weights() {
419 let cases: &[&[u16]] = &[
420 &[39, 39, 79],
421 &[40, 38, 79],
422 &[1, 1, 155],
423 &[52, 52, 53],
424 &[10, 20, 30, 40],
425 &[7, 11, 13, 17, 19],
426 &[100, 1, 1],
427 &[3, 3],
428 ];
429
430 for sizes in cases {
431 let available: u16 = sizes.iter().sum();
432 let weights = sizes_to_weights(sizes);
433 let restored = sizes_from_weights(&weights, available, 0);
434 assert_eq!(
435 restored, *sizes,
436 "round trip changed {sizes:?} (available {available})"
437 );
438 }
439 }
440
441 #[test]
443 fn leftover_columns_follow_the_largest_fraction() {
444 assert_eq!(sizes_from_weights(&[1.0, 1.0, 1.0], 10, 0), vec![4, 3, 3]);
447
448 assert_eq!(sizes_from_weights(&[0.1, 0.4, 0.5], 10, 0), vec![1, 4, 5]);
450
451 assert_eq!(
454 sizes_from_weights(&[0.09, 0.45, 0.46], 10, 0),
455 vec![1, 4, 5]
456 );
457 }
458
459 #[test]
460 fn sizes_always_fill_the_available_space() {
461 for available in [1u16, 7, 13, 80, 157, 999] {
462 for weights in [
463 vec![1.0, 1.0, 2.0],
464 vec![0.3333, 0.3333, 0.3334],
465 vec![0.01, 0.98, 0.01],
466 ] {
467 let sizes = sizes_from_weights(&weights, available, 0);
468 assert_eq!(
469 sizes.iter().sum::<u16>(),
470 available,
471 "weights {weights:?} at {available} left a gap"
472 );
473 }
474 }
475 }
476}