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