1extern crate self as escriba_ui;
4
5use escriba_core::{BufferId, Position, WindowId};
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
10pub struct Rect {
11 pub x: u32,
12 pub y: u32,
13 pub width: u32,
14 pub height: u32,
15}
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
18pub struct Viewport {
19 pub top_line: u32,
20 pub left_column: u32,
21 pub visible_lines: u32,
22 pub visible_columns: u32,
23}
24
25impl Viewport {
26 #[must_use]
36 pub fn scroll_to_contain(mut self, p: Position, margin: u32) -> Self {
37 let bot = p.line.saturating_add(margin);
39 if p.line < self.top_line {
40 self.top_line = p.line.saturating_sub(margin);
41 }
42 if bot >= self.top_line.saturating_add(self.visible_lines) {
43 self.top_line = bot.saturating_sub(self.visible_lines.saturating_sub(1));
44 }
45
46 let right = p.column.saturating_add(margin);
48 if p.column < self.left_column {
49 self.left_column = p.column.saturating_sub(margin);
50 }
51 if right >= self.left_column.saturating_add(self.visible_columns) {
52 self.left_column = right.saturating_sub(self.visible_columns.saturating_sub(1));
53 }
54 self
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
59pub struct Window {
60 pub id: WindowId,
61 pub buffer_id: BufferId,
62 pub viewport: Viewport,
63 pub rect: Rect,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
67pub struct Layout {
68 pub windows: Vec<Window>,
69 pub active: WindowId,
70 pub statusline: bool,
71 pub tabbar: bool,
72}
73
74impl Layout {
75 #[must_use]
76 pub fn single(window: Window) -> Self {
77 Self {
78 active: window.id,
79 windows: vec![window],
80 statusline: true,
81 tabbar: true,
82 }
83 }
84
85 #[must_use]
86 pub fn active_window(&self) -> Option<&Window> {
87 self.windows.iter().find(|w| w.id == self.active)
88 }
89}
90
91#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
92pub struct StatusLine {
93 pub mode: String,
94 pub path: Option<String>,
95 pub cursor: Position,
96 pub modified: bool,
97 pub line_count: u32,
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 fn small() -> Viewport {
105 Viewport {
108 top_line: 0,
109 left_column: 0,
110 visible_lines: 5,
111 visible_columns: 10,
112 }
113 }
114
115 #[test]
116 fn viewport_scrolls_down() {
117 let v = Viewport {
118 top_line: 0,
119 left_column: 0,
120 visible_lines: 20,
121 visible_columns: 80,
122 };
123 let v2 = v.scroll_to_contain(Position::new(30, 0), 2);
124 assert!(v2.top_line > 0);
125 }
126
127 #[test]
128 fn scroll_noop_when_already_visible() {
129 let v = small();
130 let v2 = v.scroll_to_contain(Position::new(2, 4), 2);
132 assert_eq!(v2, v, "an in-window position must not move the viewport");
133 }
134
135 #[test]
136 fn scroll_down_keeps_cursor_visible() {
137 let v = small();
138 let v2 = v.scroll_to_contain(Position::new(30, 0), 2);
139 assert!(
140 v2.top_line <= 30 && 30 < v2.top_line + v2.visible_lines,
141 "cursor line must be within [top_line, top_line+visible_lines): {v2:?}"
142 );
143 }
144
145 #[test]
146 fn scroll_right_keeps_cursor_visible() {
147 let v = small();
148 let v2 = v.scroll_to_contain(Position::new(0, 50), 2);
150 assert!(
151 v2.left_column <= 50 && 50 < v2.left_column + v2.visible_columns,
152 "cursor column must be within [left_column, left_column+visible_columns): {v2:?}"
153 );
154 }
155
156 #[test]
157 fn scroll_up_left_returns_toward_origin() {
158 let v = Viewport {
161 top_line: 20,
162 left_column: 30,
163 visible_lines: 5,
164 visible_columns: 10,
165 };
166 let v2 = v.scroll_to_contain(Position::new(2, 3), 2);
167 assert!(v2.top_line <= 2, "must scroll up to reveal line 2: {v2:?}");
168 assert!(v2.left_column <= 3, "must scroll left to reveal col 3: {v2:?}");
169 }
170
171 #[test]
172 fn scroll_saturates_at_origin() {
173 let v = small();
175 let v2 = v.scroll_to_contain(Position::ZERO, 2);
176 assert_eq!(v2.top_line, 0);
177 assert_eq!(v2.left_column, 0);
178 }
179
180 #[test]
181 fn scroll_respects_margin_on_both_axes() {
182 let v = small(); let v2 = v.scroll_to_contain(Position::new(4, 9), 2);
187 assert_eq!(v2.top_line, 2, "{v2:?}");
189 assert_eq!(v2.left_column, 2, "{v2:?}");
191 assert!(v2.top_line <= 4 && 4 < v2.top_line + v2.visible_lines);
193 assert!(v2.left_column <= 9 && 9 < v2.left_column + v2.visible_columns);
194 }
195
196 #[test]
197 fn layout_active_resolves() {
198 let w = Window {
199 id: WindowId(1),
200 buffer_id: BufferId(1),
201 viewport: Viewport::default(),
202 rect: Rect::default(),
203 };
204 let layout = Layout::single(w);
205 assert_eq!(layout.active_window().unwrap().id, WindowId(1));
206 }
207}