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