1extern crate self as escriba_ui;
4
5pub mod chrome;
9
10pub mod splash;
13
14pub mod syntax;
18
19pub mod picker;
23
24pub mod gutter;
28
29use escriba_core::{BufferId, Position, WindowId};
30use schemars::JsonSchema;
31use serde::{Deserialize, Serialize};
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
34pub struct Viewport {
35 pub top_line: u32,
36 pub left_column: u32,
37 pub visible_lines: u32,
38 pub visible_columns: u32,
39}
40
41impl Viewport {
42 #[must_use]
52 pub fn scroll_to_contain(mut self, p: Position, margin: u32) -> Self {
53 let bot = p.line.saturating_add(margin);
55 if p.line < self.top_line {
56 self.top_line = p.line.saturating_sub(margin);
57 }
58 if bot >= self.top_line.saturating_add(self.visible_lines) {
59 self.top_line = bot.saturating_sub(self.visible_lines.saturating_sub(1));
60 }
61
62 let right = p.column.saturating_add(margin);
64 if p.column < self.left_column {
65 self.left_column = p.column.saturating_sub(margin);
66 }
67 if right >= self.left_column.saturating_add(self.visible_columns) {
68 self.left_column = right.saturating_sub(self.visible_columns.saturating_sub(1));
69 }
70 self
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
75pub struct Window {
76 pub id: WindowId,
77 pub buffer_id: BufferId,
78 pub viewport: Viewport,
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
82pub struct Layout {
83 pub windows: Vec<Window>,
84 pub active: WindowId,
85 pub statusline: bool,
86 pub tabbar: bool,
87}
88
89impl Layout {
90 #[must_use]
91 pub fn single(window: Window) -> Self {
92 Self {
93 active: window.id,
94 windows: vec![window],
95 statusline: true,
96 tabbar: true,
97 }
98 }
99
100 #[must_use]
101 pub fn active_window(&self) -> Option<&Window> {
102 self.windows.iter().find(|w| w.id == self.active)
103 }
104}
105
106#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
107pub struct StatusLine {
108 pub mode: String,
109 pub path: Option<String>,
110 pub cursor: Position,
111 pub modified: bool,
112 pub line_count: u32,
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 fn small() -> Viewport {
120 Viewport {
123 top_line: 0,
124 left_column: 0,
125 visible_lines: 5,
126 visible_columns: 10,
127 }
128 }
129
130 #[test]
131 fn viewport_scrolls_down() {
132 let v = Viewport {
133 top_line: 0,
134 left_column: 0,
135 visible_lines: 20,
136 visible_columns: 80,
137 };
138 let v2 = v.scroll_to_contain(Position::new(30, 0), 2);
139 assert!(v2.top_line > 0);
140 }
141
142 #[test]
143 fn scroll_noop_when_already_visible() {
144 let v = small();
145 let v2 = v.scroll_to_contain(Position::new(2, 4), 2);
147 assert_eq!(v2, v, "an in-window position must not move the viewport");
148 }
149
150 #[test]
151 fn scroll_down_keeps_cursor_visible() {
152 let v = small();
153 let v2 = v.scroll_to_contain(Position::new(30, 0), 2);
154 assert!(
155 v2.top_line <= 30 && 30 < v2.top_line + v2.visible_lines,
156 "cursor line must be within [top_line, top_line+visible_lines): {v2:?}"
157 );
158 }
159
160 #[test]
161 fn scroll_right_keeps_cursor_visible() {
162 let v = small();
163 let v2 = v.scroll_to_contain(Position::new(0, 50), 2);
165 assert!(
166 v2.left_column <= 50 && 50 < v2.left_column + v2.visible_columns,
167 "cursor column must be within [left_column, left_column+visible_columns): {v2:?}"
168 );
169 }
170
171 #[test]
172 fn scroll_up_left_returns_toward_origin() {
173 let v = Viewport {
176 top_line: 20,
177 left_column: 30,
178 visible_lines: 5,
179 visible_columns: 10,
180 };
181 let v2 = v.scroll_to_contain(Position::new(2, 3), 2);
182 assert!(v2.top_line <= 2, "must scroll up to reveal line 2: {v2:?}");
183 assert!(
184 v2.left_column <= 3,
185 "must scroll left to reveal col 3: {v2:?}"
186 );
187 }
188
189 #[test]
190 fn scroll_saturates_at_origin() {
191 let v = small();
193 let v2 = v.scroll_to_contain(Position::ZERO, 2);
194 assert_eq!(v2.top_line, 0);
195 assert_eq!(v2.left_column, 0);
196 }
197
198 #[test]
199 fn scroll_respects_margin_on_both_axes() {
200 let v = small(); let v2 = v.scroll_to_contain(Position::new(4, 9), 2);
205 assert_eq!(v2.top_line, 2, "{v2:?}");
207 assert_eq!(v2.left_column, 2, "{v2:?}");
209 assert!(v2.top_line <= 4 && 4 < v2.top_line + v2.visible_lines);
211 assert!(v2.left_column <= 9 && 9 < v2.left_column + v2.visible_columns);
212 }
213
214 #[test]
215 fn layout_active_resolves() {
216 let w = Window {
217 id: WindowId(1),
218 buffer_id: BufferId(1),
219 viewport: Viewport::default(),
220 };
221 let layout = Layout::single(w);
222 assert_eq!(layout.active_window().unwrap().id, WindowId(1));
223 }
224}