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