Skip to main content

zellij_utils/
pane_size.rs

1use serde::{Deserialize, Serialize};
2use std::{
3    fmt::Display,
4    hash::{Hash, Hasher},
5};
6
7use crate::data::FloatingPaneCoordinates;
8use crate::input::layout::{PercentOrFixed, SplitDirection, SplitSize};
9use crate::position::Position;
10
11/// Contains the position and size of a [`Pane`], or more generally of any terminal, measured
12/// in character rows and columns.
13#[derive(Clone, Copy, Default, Debug, Serialize, Deserialize)]
14pub struct PaneGeom {
15    pub x: usize,
16    pub y: usize,
17    pub rows: Dimension,
18    pub cols: Dimension,
19    pub stacked: Option<usize>,          // usize - stack id
20    pub is_pinned: bool,                 // only relevant to floating panes
21    pub logical_position: Option<usize>, // relevant when placing this pane in a layout
22}
23
24impl PartialEq for PaneGeom {
25    fn eq(&self, other: &Self) -> bool {
26        // compare all except is_pinned
27        // NOTE: Keep this in sync with what the `Hash` trait impl does.
28        self.x == other.x
29            && self.y == other.y
30            && self.rows == other.rows
31            && self.cols == other.cols
32            && self.stacked == other.stacked
33    }
34}
35
36impl std::hash::Hash for PaneGeom {
37    fn hash<H: Hasher>(&self, state: &mut H) {
38        // NOTE: Keep this in sync with what the `PartiqlEq` trait impl does.
39        self.x.hash(state);
40        self.y.hash(state);
41        self.rows.hash(state);
42        self.cols.hash(state);
43        self.stacked.hash(state);
44    }
45}
46
47impl Eq for PaneGeom {}
48
49#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
50pub struct Viewport {
51    pub x: usize,
52    pub y: usize,
53    pub rows: usize,
54    pub cols: usize,
55}
56
57impl Viewport {
58    pub fn has_positive_size(&self) -> bool {
59        self.rows > 0 && self.cols > 0
60    }
61}
62
63#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
64pub struct Offset {
65    pub top: usize,
66    pub bottom: usize,
67    pub right: usize,
68    pub left: usize,
69}
70
71#[derive(Clone, Copy, Default, PartialEq, Eq, Debug, Serialize, Deserialize)]
72pub struct Size {
73    pub rows: usize,
74    pub cols: usize,
75}
76
77#[derive(Clone, Copy, Default, PartialEq, Eq, Debug, Serialize, Deserialize)]
78pub struct SizeInPixels {
79    pub height: usize,
80    pub width: usize,
81}
82
83#[derive(Clone, Copy, Default, PartialEq, Eq, Debug, Serialize, Deserialize)]
84pub struct Insets {
85    pub top: usize,
86    pub bottom: usize,
87    pub left: usize,
88    pub right: usize,
89}
90
91#[derive(Eq, Clone, Copy, PartialEq, Debug, Serialize, Deserialize, Hash)]
92pub struct Dimension {
93    pub constraint: Constraint,
94    pub(crate) inner: usize,
95}
96
97impl Default for Dimension {
98    fn default() -> Self {
99        Self::percent(100.0)
100    }
101}
102
103impl Dimension {
104    pub fn fixed(size: usize) -> Dimension {
105        Self {
106            constraint: Constraint::Fixed(size),
107            inner: size,
108        }
109    }
110
111    pub fn percent(percent: f64) -> Dimension {
112        Self {
113            constraint: Constraint::Percent(percent),
114            inner: 1,
115        }
116    }
117
118    pub fn as_usize(&self) -> usize {
119        self.inner
120    }
121
122    pub fn as_percent(&self) -> Option<f64> {
123        if let Constraint::Percent(p) = self.constraint {
124            Some(p)
125        } else {
126            None
127        }
128    }
129
130    pub fn set_percent(&mut self, percent: f64) {
131        self.constraint = Constraint::Percent(percent);
132    }
133
134    pub fn set_inner(&mut self, inner: usize) {
135        self.inner = inner;
136    }
137
138    pub fn adjust_inner(&mut self, full_size: usize) -> f64 {
139        // returns the leftover from
140        // rounding if any
141        // TODO: elsewhere?
142        match self.constraint {
143            Constraint::Percent(percent) => {
144                let new_inner = (percent / 100.0) * full_size as f64;
145                let rounded = new_inner.floor();
146                let leftover = rounded - new_inner;
147                self.set_inner(rounded as usize);
148                leftover
149            },
150            Constraint::Fixed(fixed_size) => {
151                self.set_inner(fixed_size);
152                0.0
153            },
154        }
155    }
156    pub fn increase_inner(&mut self, by: usize) {
157        self.inner += by;
158    }
159    pub fn decrease_inner(&mut self, by: usize) {
160        self.inner = self.inner.saturating_sub(by);
161    }
162
163    pub fn is_fixed(&self) -> bool {
164        matches!(self.constraint, Constraint::Fixed(_))
165    }
166    pub fn is_percent(&self) -> bool {
167        matches!(self.constraint, Constraint::Percent(_))
168    }
169    pub fn from_split_size(split_size: SplitSize, full_size: usize) -> Self {
170        match split_size {
171            SplitSize::Fixed(fixed) => Dimension {
172                constraint: Constraint::Fixed(fixed),
173                inner: fixed,
174            },
175            SplitSize::Percent(percent) => Dimension {
176                constraint: Constraint::Percent(percent as f64),
177                inner: ((percent as f64 / 100.0) * full_size as f64).floor() as usize,
178            },
179        }
180    }
181    pub fn from_percent_or_fixed(size: PercentOrFixed, full_size: usize) -> Self {
182        match size {
183            PercentOrFixed::Fixed(fixed) => Dimension {
184                constraint: Constraint::Fixed(fixed),
185                inner: fixed,
186            },
187            PercentOrFixed::Percent(percent) => Dimension {
188                constraint: Constraint::Percent(percent as f64),
189                inner: ((percent as f64 / 100.0) * full_size as f64).floor() as usize,
190            },
191        }
192    }
193    pub fn split_out(&mut self, by: f64) -> Self {
194        match self.constraint {
195            Constraint::Percent(percent) => {
196                let split_out_value = percent / by;
197                let split_out_inner_value = self.inner / by as usize;
198                self.constraint = Constraint::Percent(percent - split_out_value);
199                self.inner = self.inner.saturating_sub(split_out_inner_value);
200                let mut split_out_dimension = Self::percent(split_out_value);
201                split_out_dimension.inner = split_out_inner_value;
202                split_out_dimension
203            },
204            Constraint::Fixed(fixed) => {
205                let split_out_value = fixed / by as usize;
206                self.constraint = Constraint::Fixed(fixed - split_out_value);
207                Self::fixed(split_out_value)
208            },
209        }
210    }
211    pub fn reduce_by(&mut self, by: f64, by_inner: usize) {
212        match self.constraint {
213            Constraint::Percent(percent) => {
214                self.constraint = Constraint::Percent(percent - by);
215                self.inner = self.inner.saturating_sub(by_inner);
216            },
217            Constraint::Fixed(_fixed) => {
218                log::error!("Cannot reduce_by fixed dimensions");
219            },
220        }
221    }
222}
223
224#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
225pub enum Constraint {
226    /// Constrains the dimension to a fixed, integer number of rows / columns
227    Fixed(usize),
228    /// Constrains the dimension to a flexible percent size of the total screen
229    Percent(f64),
230}
231
232impl Display for Constraint {
233    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234        let actual = match self {
235            Constraint::Fixed(v) => *v as f64,
236            Constraint::Percent(v) => *v,
237        };
238        write!(f, "{}", actual)?;
239        Ok(())
240    }
241}
242
243#[allow(clippy::derive_hash_xor_eq)]
244impl Hash for Constraint {
245    fn hash<H: Hasher>(&self, state: &mut H) {
246        match self {
247            Constraint::Fixed(size) => size.hash(state),
248            Constraint::Percent(size) => (*size as usize).hash(state),
249        }
250    }
251}
252
253impl Eq for Constraint {}
254
255impl PaneGeom {
256    pub fn contains(&self, point: &Position) -> bool {
257        let col = point.column.0 as usize;
258        let row = point.line.0 as usize;
259        self.x <= col
260            && col < self.x + self.cols.as_usize()
261            && self.y <= row
262            && row < self.y + self.rows.as_usize()
263    }
264    pub fn is_at_least_minimum_size(&self) -> bool {
265        self.rows.as_usize() > 0 && self.cols.as_usize() > 0
266    }
267    pub fn is_flexible_in_direction(&self, split_direction: SplitDirection) -> bool {
268        match split_direction {
269            SplitDirection::Vertical => self.cols.is_percent(),
270            SplitDirection::Horizontal => self.rows.is_percent(),
271        }
272    }
273    pub fn apply_floating_pane_position(
274        &mut self,
275        x: Option<PercentOrFixed>,
276        y: Option<PercentOrFixed>,
277        width: Option<PercentOrFixed>,
278        height: Option<PercentOrFixed>,
279        viewport_cols: usize,
280        viewport_rows: usize,
281    ) {
282        if let Some(width) = &width {
283            self.cols = Dimension::fixed(width.to_fixed(viewport_cols));
284        }
285        if let Some(height) = &height {
286            self.rows = Dimension::fixed(height.to_fixed(viewport_rows));
287        }
288        self.x = match &x {
289            Some(x) => x.to_fixed(viewport_cols),
290            None if width.is_some() => viewport_cols.saturating_sub(self.cols.as_usize()) / 2,
291            None => self.x,
292        };
293        self.y = match &y {
294            Some(y) => y.to_fixed(viewport_rows),
295            None if height.is_some() => viewport_rows.saturating_sub(self.rows.as_usize()) / 2,
296            None => self.y,
297        };
298    }
299    pub fn adjust_coordinates(
300        &mut self,
301        floating_pane_coordinates: FloatingPaneCoordinates,
302        viewport: Viewport,
303    ) {
304        self.apply_floating_pane_position(
305            floating_pane_coordinates.x,
306            floating_pane_coordinates.y,
307            floating_pane_coordinates.width,
308            floating_pane_coordinates.height,
309            viewport.cols,
310            viewport.rows,
311        );
312        if self.x < viewport.x {
313            self.x = viewport.x;
314        } else if self.x > viewport.x + viewport.cols {
315            self.x = (viewport.x + viewport.cols).saturating_sub(self.cols.as_usize());
316        }
317        if self.y < viewport.y {
318            self.y = viewport.y;
319        } else if self.y > viewport.y + viewport.rows {
320            self.y = (viewport.y + viewport.rows).saturating_sub(self.rows.as_usize());
321        }
322        if self.x + self.cols.as_usize() > viewport.x + viewport.cols {
323            let new_cols = (viewport.x + viewport.cols).saturating_sub(self.x);
324            self.cols.set_inner(new_cols);
325        }
326        if self.y + self.rows.as_usize() > viewport.y + viewport.rows {
327            let new_rows = (viewport.y + viewport.rows).saturating_sub(self.y);
328            self.rows.set_inner(new_rows);
329        }
330    }
331    pub fn combine_vertically_with(&self, geom_below: &PaneGeom) -> Option<Self> {
332        match (self.rows.constraint, geom_below.rows.constraint) {
333            (Constraint::Percent(self_percent), Constraint::Percent(geom_below_percent)) => {
334                let mut combined = self.clone();
335                combined.rows = Dimension::percent(self_percent + geom_below_percent);
336                combined.rows.inner = self.rows.inner + geom_below.rows.inner;
337                Some(combined)
338            },
339            _ => {
340                log::error!("Can't combine fixed panes");
341                None
342            },
343        }
344    }
345    pub fn combine_horizontally_with(&self, geom_to_the_right: &PaneGeom) -> Option<Self> {
346        match (self.cols.constraint, geom_to_the_right.cols.constraint) {
347            (Constraint::Percent(self_percent), Constraint::Percent(geom_to_the_right_percent)) => {
348                let mut combined = self.clone();
349                combined.cols = Dimension::percent(self_percent + geom_to_the_right_percent);
350                combined.cols.inner = self.cols.inner + geom_to_the_right.cols.inner;
351                Some(combined)
352            },
353            _ => {
354                log::error!("Can't combine fixed panes");
355                None
356            },
357        }
358    }
359    pub fn combine_vertically_with_many(&self, geoms_below: &Vec<PaneGeom>) -> Option<Self> {
360        // here we expect the geoms to be sorted by their y and be contiguous (i.e. same x and
361        // width, no overlaps) and be below self
362        let mut combined = self.clone();
363        for geom_below in geoms_below {
364            match (combined.rows.constraint, geom_below.rows.constraint) {
365                (
366                    Constraint::Percent(combined_percent),
367                    Constraint::Percent(geom_below_percent),
368                ) => {
369                    let new_rows_inner = combined.rows.inner + geom_below.rows.inner;
370                    combined.rows = Dimension::percent(combined_percent + geom_below_percent);
371                    combined.rows.inner = new_rows_inner;
372                },
373                _ => {
374                    log::error!("Can't combine fixed panes");
375                    return None;
376                },
377            }
378        }
379        Some(combined)
380    }
381    pub fn combine_horizontally_with_many(
382        &self,
383        geoms_to_the_right: &Vec<PaneGeom>,
384    ) -> Option<Self> {
385        // here we expect the geoms to be sorted by their x and be contiguous (i.e. same x and
386        // width, no overlaps) and be right of self
387        let mut combined = self.clone();
388        for geom_to_the_right in geoms_to_the_right {
389            match (combined.cols.constraint, geom_to_the_right.cols.constraint) {
390                (
391                    Constraint::Percent(combined_percent),
392                    Constraint::Percent(geom_to_the_right_percent),
393                ) => {
394                    let new_cols = combined.cols.inner + geom_to_the_right.cols.inner;
395                    combined.cols =
396                        Dimension::percent(combined_percent + geom_to_the_right_percent);
397                    combined.cols.inner = new_cols;
398                },
399                _ => {
400                    log::error!("Can't combine fixed panes");
401                    return None;
402                },
403            }
404        }
405        Some(combined)
406    }
407    pub fn is_stacked(&self) -> bool {
408        self.stacked.is_some()
409    }
410}
411
412impl Display for PaneGeom {
413    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
414        write!(f, "{{ ")?;
415        write!(f, r#""x": {},"#, self.x)?;
416        write!(f, r#""y": {},"#, self.y)?;
417        write!(f, r#""cols": {},"#, self.cols.constraint)?;
418        write!(f, r#""rows": {},"#, self.rows.constraint)?;
419        write!(f, r#""stacked": {:?}"#, self.stacked)?;
420        write!(f, r#""logical_position": {:?}"#, self.logical_position)?;
421        write!(f, " }}")?;
422
423        Ok(())
424    }
425}
426
427impl Offset {
428    pub fn frame(size: usize) -> Self {
429        Self {
430            top: size,
431            bottom: size,
432            right: size,
433            left: size,
434        }
435    }
436
437    pub fn shift_right_and_top(right: usize, top: usize) -> Self {
438        Self {
439            right,
440            top,
441            ..Default::default()
442        }
443    }
444
445    pub fn shift_right(right: usize) -> Self {
446        Self {
447            right,
448            ..Default::default()
449        }
450    }
451
452    pub fn shift_right_top_and_bottom(right: usize, top: usize, bottom: usize) -> Self {
453        Self {
454            right,
455            top,
456            bottom,
457            ..Default::default()
458        }
459    }
460
461    // FIXME: This should be top and left, not bottom and right, but `boundaries.rs` would need
462    // some changing
463    pub fn shift(bottom: usize, right: usize) -> Self {
464        Self {
465            bottom,
466            right,
467            ..Default::default()
468        }
469    }
470}
471
472impl From<PaneGeom> for Viewport {
473    fn from(pane: PaneGeom) -> Self {
474        Self {
475            x: pane.x,
476            y: pane.y,
477            rows: pane.rows.as_usize(),
478            cols: pane.cols.as_usize(),
479        }
480    }
481}
482
483impl From<Size> for Viewport {
484    fn from(size: Size) -> Self {
485        Self {
486            rows: size.rows,
487            cols: size.cols,
488            ..Default::default()
489        }
490    }
491}
492
493impl From<&PaneGeom> for Size {
494    fn from(pane_geom: &PaneGeom) -> Self {
495        Self {
496            rows: pane_geom.rows.as_usize(),
497            cols: pane_geom.cols.as_usize(),
498        }
499    }
500}
501
502impl From<&Size> for PaneGeom {
503    fn from(size: &Size) -> Self {
504        let mut rows = Dimension::percent(100.0);
505        let mut cols = Dimension::percent(100.0);
506        rows.set_inner(size.rows);
507        cols.set_inner(size.cols);
508        Self {
509            rows,
510            cols,
511            ..Default::default()
512        }
513    }
514}