1extern crate self as escriba_ui;
4
5pub mod chrome;
9
10pub mod splash;
13
14pub mod syntax;
18
19pub mod shikiri;
22
23pub mod picker;
27
28pub mod gutter;
32
33use escriba_core::{BufferId, Position, WindowId};
34use schemars::JsonSchema;
35use serde::{Deserialize, Serialize};
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
38pub struct Viewport {
39 pub top_line: u32,
40 pub left_column: u32,
41 pub visible_lines: u32,
42 pub visible_columns: u32,
43}
44
45impl Viewport {
46 #[must_use]
56 pub fn scroll_to_contain(mut self, p: Position, margin: u32) -> Self {
57 let bot = p.line.saturating_add(margin);
59 if p.line < self.top_line {
60 self.top_line = p.line.saturating_sub(margin);
61 }
62 if bot >= self.top_line.saturating_add(self.visible_lines) {
63 self.top_line = bot.saturating_sub(self.visible_lines.saturating_sub(1));
64 }
65
66 let right = p.column.saturating_add(margin);
68 if p.column < self.left_column {
69 self.left_column = p.column.saturating_sub(margin);
70 }
71 if right >= self.left_column.saturating_add(self.visible_columns) {
72 self.left_column = right.saturating_sub(self.visible_columns.saturating_sub(1));
73 }
74 self
75 }
76}
77
78pub use shikiri::Window;
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
83pub struct Layout {
84 tree: shikiri::Shikiri,
87 active: WindowId,
89 frame: shikiri::Rect,
92 next_id: u64,
93 pub statusline: bool,
94 pub tabbar: bool,
95}
96
97impl Layout {
98 #[must_use]
99 pub fn single(window: Window) -> Self {
100 Self {
101 active: window.id,
102 next_id: window.id.0 + 1,
103 tree: shikiri::Shikiri::Pane(window),
104 frame: shikiri::Rect::default(),
105 statusline: true,
106 tabbar: true,
107 }
108 }
109
110 #[must_use]
111 pub const fn active(&self) -> WindowId {
112 self.active
113 }
114
115 pub fn focus(&mut self, id: WindowId) -> bool {
117 if self.windows().any(|w| w.id == id) {
118 self.active = id;
119 true
120 } else {
121 false
122 }
123 }
124
125 pub fn set_frame(&mut self, frame: shikiri::Rect) {
128 self.frame = frame;
129 }
130
131 #[must_use]
132 pub const fn frame(&self) -> shikiri::Rect {
133 self.frame
134 }
135
136 #[must_use]
139 pub const fn tree(&self) -> &shikiri::Shikiri {
140 &self.tree
141 }
142
143 #[must_use]
145 pub fn solved(&self) -> shikiri::Solved {
146 shikiri::solve(&self.tree, self.frame)
147 }
148
149 pub fn windows(&self) -> impl Iterator<Item = &Window> {
151 fn walk<'a>(n: &'a shikiri::Shikiri, out: &mut Vec<&'a Window>) {
152 match n {
153 shikiri::Shikiri::Pane(w) => out.push(w),
154 shikiri::Shikiri::Split(s) => s.children().for_each(|c| walk(c, out)),
155 }
156 }
157 let mut v = Vec::new();
158 walk(&self.tree, &mut v);
159 v.into_iter()
160 }
161
162 pub fn windows_mut(&mut self) -> Vec<&mut Window> {
164 fn walk<'a>(n: &'a mut shikiri::Shikiri, out: &mut Vec<&'a mut Window>) {
165 match n {
166 shikiri::Shikiri::Pane(w) => out.push(w),
167 shikiri::Shikiri::Split(s) => s.children_mut().for_each(|c| walk(c, out)),
168 }
169 }
170 let mut v = Vec::new();
171 walk(&mut self.tree, &mut v);
172 v
173 }
174
175 #[must_use]
176 pub fn active_window(&self) -> Option<&Window> {
177 let id = self.active;
178 self.windows().find(|w| w.id == id)
179 }
180
181 pub fn active_window_mut(&mut self) -> Option<&mut Window> {
182 let id = self.active;
183 self.windows_mut().into_iter().find(|w| w.id == id)
184 }
185
186 #[must_use]
187 pub fn count(&self) -> usize {
188 self.windows().count()
189 }
190
191 pub fn split_active(&mut self, axis: shikiri::Axis) -> WindowId {
199 let id = WindowId(self.next_id);
200 self.next_id += 1;
201 let active = self.active;
202 let fresh = self
203 .active_window()
204 .map(|w| Window {
205 id,
206 buffer_id: w.buffer_id,
207 viewport: w.viewport,
208 })
209 .unwrap_or(Window {
210 id,
211 buffer_id: BufferId(0),
212 viewport: Viewport::default(),
213 });
214 split_at(&mut self.tree, active, axis, fresh);
215 self.active = id;
216 id
217 }
218
219 pub fn close(&mut self, id: WindowId) -> bool {
222 if self.count() <= 1 {
223 return false;
224 }
225 if self.active == id {
227 let next = self
228 .windows()
229 .map(|w| w.id)
230 .find(|w| *w != id)
231 .unwrap_or(id);
232 self.active = next;
233 }
234 remove(&mut self.tree, id)
235 }
236
237 #[must_use]
243 pub fn neighbour(&self, dir: Dir) -> Option<WindowId> {
244 let solved = self.solved();
245 let here = solved.rect_of(self.active)?;
246 solved
247 .panes
248 .iter()
249 .filter(|(id, _)| *id != self.active)
250 .filter(|(_, r)| match dir {
251 Dir::Left => r.x + r.w <= here.x,
252 Dir::Right => r.x >= here.x + here.w,
253 Dir::Up => r.y + r.h <= here.y,
254 Dir::Down => r.y >= here.y + here.h,
255 })
256 .min_by_key(|(_, r)| match dir {
260 Dir::Left => (here.x.saturating_sub(r.x + r.w), here.y.abs_diff(r.y)),
261 Dir::Right => (r.x.saturating_sub(here.x + here.w), here.y.abs_diff(r.y)),
262 Dir::Up => (here.y.saturating_sub(r.y + r.h), here.x.abs_diff(r.x)),
263 Dir::Down => (r.y.saturating_sub(here.y + here.h), here.x.abs_diff(r.x)),
264 })
265 .map(|(id, _)| *id)
266 }
267}
268
269#[derive(Debug, Clone, Copy, PartialEq, Eq)]
271pub enum Dir {
272 Left,
273 Right,
274 Up,
275 Down,
276}
277
278fn split_at(
280 node: &mut shikiri::Shikiri,
281 target: WindowId,
282 axis: shikiri::Axis,
283 fresh: Window,
284) -> bool {
285 match node {
286 shikiri::Shikiri::Pane(w) if w.id == target => {
287 let existing = std::mem::replace(node, shikiri::Shikiri::Pane(fresh.clone()));
288 *node = shikiri::Shikiri::Split(shikiri::Split::new(
289 axis,
290 shikiri::Shikiri::Pane(fresh),
291 existing,
292 ));
293 true
294 }
295 shikiri::Shikiri::Pane(_) => false,
296 shikiri::Shikiri::Split(s) => s
297 .children_mut()
298 .any(|c| split_at(c, target, axis, fresh.clone())),
299 }
300}
301
302fn remove(node: &mut shikiri::Shikiri, id: WindowId) -> bool {
304 let shikiri::Shikiri::Split(s) = node else {
305 return false;
306 };
307 if let Some(rest) = s.without(id) {
308 *node = rest;
309 return true;
310 }
311 let shikiri::Shikiri::Split(s) = node else {
312 return false;
313 };
314 s.children_mut().any(|c| remove(c, id))
315}
316
317#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
318pub struct StatusLine {
319 pub mode: String,
320 pub path: Option<String>,
321 pub cursor: Position,
322 pub modified: bool,
323 pub line_count: u32,
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 fn small() -> Viewport {
331 Viewport {
334 top_line: 0,
335 left_column: 0,
336 visible_lines: 5,
337 visible_columns: 10,
338 }
339 }
340
341 #[test]
342 fn viewport_scrolls_down() {
343 let v = Viewport {
344 top_line: 0,
345 left_column: 0,
346 visible_lines: 20,
347 visible_columns: 80,
348 };
349 let v2 = v.scroll_to_contain(Position::new(30, 0), 2);
350 assert!(v2.top_line > 0);
351 }
352
353 #[test]
354 fn scroll_noop_when_already_visible() {
355 let v = small();
356 let v2 = v.scroll_to_contain(Position::new(2, 4), 2);
358 assert_eq!(v2, v, "an in-window position must not move the viewport");
359 }
360
361 #[test]
362 fn scroll_down_keeps_cursor_visible() {
363 let v = small();
364 let v2 = v.scroll_to_contain(Position::new(30, 0), 2);
365 assert!(
366 v2.top_line <= 30 && 30 < v2.top_line + v2.visible_lines,
367 "cursor line must be within [top_line, top_line+visible_lines): {v2:?}"
368 );
369 }
370
371 #[test]
372 fn scroll_right_keeps_cursor_visible() {
373 let v = small();
374 let v2 = v.scroll_to_contain(Position::new(0, 50), 2);
376 assert!(
377 v2.left_column <= 50 && 50 < v2.left_column + v2.visible_columns,
378 "cursor column must be within [left_column, left_column+visible_columns): {v2:?}"
379 );
380 }
381
382 #[test]
383 fn scroll_up_left_returns_toward_origin() {
384 let v = Viewport {
387 top_line: 20,
388 left_column: 30,
389 visible_lines: 5,
390 visible_columns: 10,
391 };
392 let v2 = v.scroll_to_contain(Position::new(2, 3), 2);
393 assert!(v2.top_line <= 2, "must scroll up to reveal line 2: {v2:?}");
394 assert!(
395 v2.left_column <= 3,
396 "must scroll left to reveal col 3: {v2:?}"
397 );
398 }
399
400 #[test]
401 fn scroll_saturates_at_origin() {
402 let v = small();
404 let v2 = v.scroll_to_contain(Position::ZERO, 2);
405 assert_eq!(v2.top_line, 0);
406 assert_eq!(v2.left_column, 0);
407 }
408
409 #[test]
410 fn scroll_respects_margin_on_both_axes() {
411 let v = small(); let v2 = v.scroll_to_contain(Position::new(4, 9), 2);
416 assert_eq!(v2.top_line, 2, "{v2:?}");
418 assert_eq!(v2.left_column, 2, "{v2:?}");
420 assert!(v2.top_line <= 4 && 4 < v2.top_line + v2.visible_lines);
422 assert!(v2.left_column <= 9 && 9 < v2.left_column + v2.visible_columns);
423 }
424
425 #[test]
426 fn layout_active_resolves() {
427 let w = Window {
428 id: WindowId(1),
429 buffer_id: BufferId(1),
430 viewport: Viewport::default(),
431 };
432 let layout = Layout::single(w);
433 assert_eq!(layout.active_window().unwrap().id, WindowId(1));
434 }
435}