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