1use serde::{Deserialize, Serialize};
9use serde_json::Value;
10
11pub use crate::layout::{PANEL_MIN, RESIZE_HANDLE_HIT, RESIZE_STEP};
12
13#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
15pub struct PanelLimits {
16 #[serde(default, skip_serializing_if = "Option::is_none")]
17 pub min: Option<f64>,
18 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub max: Option<f64>,
20 #[serde(default)]
22 pub collapsible: bool,
23}
24
25impl PanelLimits {
26 pub fn list(v: Option<&Value>) -> Vec<PanelLimits> {
28 v.and_then(Value::as_array)
29 .map(|a| {
30 a.iter()
31 .map(|p| PanelLimits {
32 min: p.get("min").and_then(Value::as_f64),
33 max: p.get("max").and_then(Value::as_f64),
34 collapsible: p.get("collapsible") == Some(&Value::Bool(true)),
35 })
36 .collect()
37 })
38 .unwrap_or_default()
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
44#[serde(rename_all = "lowercase")]
45pub enum Orientation {
46 #[default]
47 Horizontal,
48 Vertical,
49}
50
51impl Orientation {
52 pub fn parse(s: &str) -> Option<Orientation> {
53 match s {
54 "horizontal" => Some(Orientation::Horizontal),
55 "vertical" => Some(Orientation::Vertical),
56 _ => None,
57 }
58 }
59}
60
61const EPS: f64 = 1e-9;
62
63fn clamp(v: f64, lo: f64, hi: f64) -> f64 {
64 v.min(hi).max(lo)
65}
66
67fn tidy(v: f64) -> f64 {
69 let r = (v * 1e6).round() / 1e6;
70 if r == 0.0 {
71 0.0
72 } else {
73 r
74 }
75}
76
77struct Lim {
78 min: f64,
79 max: f64,
80 collapsible: bool,
81}
82
83fn limits_of(limits: &[PanelLimits], i: usize, count: usize) -> Lim {
84 let l = limits.get(i).copied().unwrap_or_default();
85 let cap = 100.0 / count.max(1) as f64;
86 let min = clamp(l.min.filter(|m| m.is_finite()).unwrap_or(PANEL_MIN), 0.0, cap);
87 let max = clamp(l.max.filter(|m| m.is_finite()).unwrap_or(100.0), min, 100.0);
88 Lim { min, max, collapsible: l.collapsible }
89}
90
91pub fn normalize_sizes(sizes: Option<&[Value]>, count: usize, limits: &[PanelLimits]) -> Vec<f64> {
97 if count == 0 {
98 return Vec::new();
99 }
100 let given: Vec<Option<f64>> = (0..count).map(|i| sizes.and_then(|s| s.get(i)).and_then(Value::as_f64).filter(|v| v.is_finite() && *v >= 0.0)).collect();
101 let missing = given.iter().filter(|v| v.is_none()).count();
102 let mut out: Vec<f64> = if missing == count {
103 vec![100.0 / count as f64; count]
104 } else {
105 let sum: f64 = given.iter().map(|v| v.unwrap_or(0.0)).sum();
106 let rest = (100.0 - sum).max(0.0);
107 given.iter().map(|v| v.unwrap_or(rest / missing as f64)).collect()
108 };
109 let total: f64 = out.iter().sum();
110 out = if total > EPS { out.iter().map(|v| v * 100.0 / total).collect() } else { vec![100.0 / count as f64; count] };
111 let lim: Vec<Lim> = (0..count).map(|i| limits_of(limits, i, count)).collect();
112 for (i, v) in out.iter_mut().enumerate() {
113 *v = if lim[i].collapsible && *v <= EPS { 0.0 } else { clamp(*v, lim[i].min, lim[i].max) };
114 }
115 let mut diff = 100.0 - out.iter().sum::<f64>();
116 for i in 0..count {
117 if diff.abs() <= EPS {
118 break;
119 }
120 if lim[i].collapsible && out[i] == 0.0 {
121 continue;
122 }
123 let room = if diff > 0.0 { lim[i].max - out[i] } else { lim[i].min - out[i] };
124 let take = if diff > 0.0 { diff.min(room) } else { diff.max(room) };
125 out[i] += take;
126 diff -= take;
127 }
128 out.into_iter().map(tidy).collect()
129}
130
131pub fn resize_panels(sizes: &[f64], handle: usize, delta: f64, limits: &[PanelLimits]) -> Vec<f64> {
138 let mut out = sizes.to_vec();
139 let (a, b) = (handle, handle + 1);
140 if b >= sizes.len() || !delta.is_finite() {
141 return out.into_iter().map(tidy).collect();
142 }
143 let la = limits_of(limits, a, sizes.len());
144 let lb = limits_of(limits, b, sizes.len());
145 let lo = (la.min - sizes[a]).max(sizes[b] - lb.max);
146 let hi = (la.max - sizes[a]).min(sizes[b] - lb.min);
147 let mut d = clamp(delta, lo.min(0.0), hi.max(0.0));
148 if la.collapsible && delta < 0.0 && sizes[a] > 0.0 && sizes[a] + delta < la.min / 2.0 && sizes[b] + sizes[a] <= lb.max + EPS {
149 d = -sizes[a];
150 } else if lb.collapsible && delta > 0.0 && sizes[b] > 0.0 && sizes[b] - delta < lb.min / 2.0 && sizes[a] + sizes[b] <= la.max + EPS {
151 d = sizes[b];
152 } else if la.collapsible && sizes[a] == 0.0 {
153 d = if delta >= la.min / 2.0 && hi >= la.min - EPS { clamp(delta, la.min, hi) } else { 0.0 };
157 } else if lb.collapsible && sizes[b] == 0.0 {
158 d = if -delta >= lb.min / 2.0 && -lo >= lb.min - EPS { -clamp(-delta, lb.min, -lo) } else { 0.0 };
159 }
160 out[a] = sizes[a] + d;
161 out[b] = sizes[b] - d;
162 out.into_iter().map(tidy).collect()
163}
164
165pub const RESIZE_KEYS: [&str; 7] = ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End", "Enter"];
168
169pub fn keyboard_resize(sizes: &[f64], handle: usize, key: &str, orientation: Orientation, rtl: bool, limits: &[PanelLimits]) -> Vec<f64> {
176 let same = || sizes.iter().copied().map(tidy).collect::<Vec<_>>();
177 let (a, b) = (handle, handle + 1);
178 if b >= sizes.len() {
179 return same();
180 }
181 let la = limits_of(limits, a, sizes.len());
182 let lb = limits_of(limits, b, sizes.len());
183 let flip = if orientation == Orientation::Horizontal && rtl { -1.0 } else { 1.0 };
184 let step = |sign: f64| resize_panels(sizes, handle, sign * RESIZE_STEP, limits);
185 let horizontal = orientation == Orientation::Horizontal;
186 match key {
187 "ArrowRight" if horizontal => step(flip),
188 "ArrowLeft" if horizontal => step(-flip),
189 "ArrowDown" if !horizontal => step(1.0),
190 "ArrowUp" if !horizontal => step(-1.0),
191 "Home" => resize_panels(sizes, handle, la.min - sizes[a], limits),
192 "End" => resize_panels(sizes, handle, la.max - sizes[a], limits),
193 "Enter" => {
194 let mut out = sizes.to_vec();
195 if la.collapsible {
196 let d = if sizes[a] > 0.0 { -sizes[a] } else { la.min.min(sizes[b] - lb.min) };
197 if sizes[b] - d > lb.max + EPS || d == 0.0 {
198 return same();
199 }
200 out[a] += d;
201 out[b] -= d;
202 } else if lb.collapsible {
203 let d = if sizes[b] > 0.0 { sizes[b] } else { -lb.min.min(sizes[a] - la.min) };
204 if sizes[a] + d > la.max + EPS || d == 0.0 {
205 return same();
206 }
207 out[a] += d;
208 out[b] -= d;
209 }
210 out.into_iter().map(tidy).collect()
211 }
212 _ => same(),
213 }
214}
215
216pub fn panel_extents(sizes: &[f64], container: f64, handle_extent: f64) -> Vec<f64> {
219 let avail = (container - sizes.len().saturating_sub(1) as f64 * handle_extent).max(0.0);
220 sizes.iter().map(|s| tidy(avail * s / 100.0)).collect()
221}
222
223pub fn drag_delta(px: f64, container: f64, panel_count: usize, orientation: Orientation, rtl: bool, handle_extent: f64) -> f64 {
226 let avail = (container - panel_count.saturating_sub(1) as f64 * handle_extent).max(1.0);
227 let flip = if orientation == Orientation::Horizontal && rtl { -1.0 } else { 1.0 };
228 tidy(px * 100.0 / avail * flip)
229}