Skip to content

Commit 6ce4461

Browse files
mgreenegitCopilot
andcommitted
Introduce native CSS-style Grid for the editor layout
Add typed row and column tracks with shared sizing and bounded placement, and adopt Grid for the viewport and editor. Cover layout geometry, resizing, focus, and existing menu interaction without changing legacy table callers. First slice of microsoft#950, microsoft#961, and microsoft#962. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 3cff89e commit 6ce4461

5 files changed

Lines changed: 590 additions & 14 deletions

File tree

‎crates/edit/src/bin/edit/draw_editor.rs‎

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,16 @@ use crate::localization::*;
1212
use crate::state::*;
1313

1414
pub fn draw_editor(ctx: &mut Context, state: &mut State) {
15+
ctx.block_begin("editor");
16+
ctx.inherit_focus();
17+
ctx.attr_display(Display::Grid);
18+
ctx.attr_grid_template_columns(&[GridTrack::Fraction(1)]);
19+
ctx.attr_grid_template_rows(&[GridTrack::Auto, GridTrack::Fraction(1)]);
20+
ctx.block_begin("search-container");
1521
if !matches!(state.wants_search.kind, StateSearchKind::Hidden | StateSearchKind::Disabled) {
1622
draw_search(ctx, state);
1723
}
18-
19-
let size = ctx.size();
20-
// TODO: The layout code should be able to just figure out the height on its own.
21-
let height_reduction = match state.wants_search.kind {
22-
StateSearchKind::Search => 4,
23-
StateSearchKind::Replace => 5,
24-
_ => 2,
25-
};
24+
ctx.block_end();
2625

2726
if let Some(doc) = state.documents.active() {
2827
ctx.textarea("textarea", doc.buffer.clone());
@@ -32,7 +31,7 @@ pub fn draw_editor(ctx: &mut Context, state: &mut State) {
3231
ctx.block_end();
3332
}
3433

35-
ctx.attr_intrinsic_size(Size { width: 0, height: size.height - height_reduction });
34+
ctx.block_end();
3635
}
3736

3837
fn draw_search(ctx: &mut Context, state: &mut State) {
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use edit::icu;
5+
use edit::input::Input;
6+
7+
use super::*;
8+
9+
fn outer(layout: &str, classname: &str) -> Rect {
10+
let node = layout.split_once(&format!("classname: {classname}\r\n")).unwrap().1;
11+
let values = node.split_once("outer: {").unwrap().1.split_once('}').unwrap().0;
12+
let values: Vec<_> = values.split(", ").map(|value| value.parse().unwrap()).collect();
13+
let [left, top, right, bottom]: [CoordType; 4] = values.try_into().unwrap();
14+
Rect { left, top, right, bottom }
15+
}
16+
17+
fn editor(lines: Option<usize>) -> (sys::Deinit, Tui, State) {
18+
let sys = sys::init().unwrap();
19+
arena::init(32 * MEBI).unwrap();
20+
icu::init().unwrap();
21+
let mut state = State::new().unwrap();
22+
if let Some(lines) = lines {
23+
let doc = state.documents.add_untitled().unwrap();
24+
let mut buffer = doc.buffer.borrow_mut();
25+
buffer.set_crlf(false);
26+
buffer.write_raw(&b"line\n".repeat(lines));
27+
}
28+
(sys, Tui::new().unwrap(), state)
29+
}
30+
31+
fn frame(tui: &mut Tui, state: &mut State, input: Option<Input<'_>>) {
32+
crate::draw(tui, input, state);
33+
for _ in 0..10 {
34+
if !tui.needs_settling() {
35+
assert_eq!(state.error_log_count, 0);
36+
return;
37+
}
38+
crate::draw(tui, None, state);
39+
}
40+
assert!(!tui.needs_settling(), "Editor layout did not settle");
41+
assert_eq!(state.error_log_count, 0);
42+
}
43+
44+
fn resize(tui: &mut Tui, state: &mut State, width: CoordType, height: CoordType) {
45+
frame(tui, state, Some(Input::Resize(Size { width, height })));
46+
}
47+
48+
#[test]
49+
fn grid_editor_geometry_tracks_search_documents_and_resize() {
50+
use StateSearchKind::{Disabled, Hidden, Replace, Search};
51+
52+
for lines in [None, Some(0), Some(1000)] {
53+
let (_sys, mut tui, mut state) = editor(lines);
54+
for width in [1, 2, 80] {
55+
for search in [Hidden, Disabled, Search, Replace] {
56+
for height in [24, 8, 7, 6, 5, 4, 3, 2, 1, 80, 32767, 24] {
57+
state.wants_search.kind = search;
58+
resize(&mut tui, &mut state, width, height);
59+
let scratch = arena::scratch_arena(None);
60+
let layout = tui.debug_layout(&scratch);
61+
let name = if lines.is_some() { "textarea" } else { "empty" };
62+
let editor = outer(&layout, name);
63+
let search_height = match state.wants_search.kind {
64+
Search => 2,
65+
Replace => 3,
66+
_ => 0,
67+
};
68+
assert_eq!(
69+
editor.height(),
70+
(height - 2 - search_height).max(0),
71+
"width={width} height={height} search_height={search_height}",
72+
);
73+
assert!(editor.width() >= 0);
74+
assert_eq!(outer(&layout, "statusbar").bottom, height);
75+
}
76+
}
77+
}
78+
}
79+
}
80+
81+
#[test]
82+
fn grid_editor_preserves_focus_through_resize_search_and_menus() {
83+
for lines in [0, 1000] {
84+
let (_sys, mut tui, mut state) = editor(Some(lines));
85+
for height in [24, 1, 24, 3, 24] {
86+
resize(&mut tui, &mut state, 80, height);
87+
}
88+
if lines > 0 {
89+
frame(&mut tui, &mut state, Some(Input::Keyboard(kbmod::CTRL | vk::HOME)));
90+
frame(&mut tui, &mut state, Some(Input::Keyboard(vk::NEXT)));
91+
let doc = state.documents.active().unwrap();
92+
assert_eq!(doc.buffer.borrow().cursor_logical_pos().y, 21);
93+
frame(&mut tui, &mut state, Some(Input::Keyboard(kbmod::CTRL | vk::END)));
94+
}
95+
let mut expected = "line\n".repeat(lines);
96+
for phase in ["resize", "search", "menu"] {
97+
if phase == "search" {
98+
state.wants_search.kind = StateSearchKind::Search;
99+
state.wants_search.focus = true;
100+
frame(&mut tui, &mut state, None);
101+
frame(&mut tui, &mut state, Some(Input::Text("needle")));
102+
assert_eq!(state.search_needle, "needle");
103+
frame(&mut tui, &mut state, Some(Input::Keyboard(vk::ESCAPE)));
104+
assert!(state.wants_search.kind == StateSearchKind::Hidden);
105+
}
106+
if phase == "menu" {
107+
frame(&mut tui, &mut state, Some(Input::Keyboard(vk::F10)));
108+
{
109+
let scratch = arena::scratch_arena(None);
110+
let layout = tui.debug_layout(&scratch);
111+
assert!(layout.contains("classname: flyout\r\n"));
112+
assert_eq!(outer(&layout, "textarea").height(), 22);
113+
assert_eq!(outer(&layout, "statusbar").bottom, 24);
114+
}
115+
frame(&mut tui, &mut state, Some(Input::Keyboard(vk::ESCAPE)));
116+
}
117+
frame(&mut tui, &mut state, Some(Input::Text(phase)));
118+
expected.push_str(phase);
119+
let mut text = String::new();
120+
state.documents.active().unwrap().buffer.borrow_mut().save_as_string(&mut text);
121+
let ending = if cfg!(windows) { "" } else { "\n" };
122+
assert_eq!(text, format!("{expected}{ending}"));
123+
let scratch = arena::scratch_arena(None);
124+
assert!(tui.render(&scratch).contains(phase), "Typed text must be visible");
125+
}
126+
}
127+
}

‎crates/edit/src/bin/edit/main.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ mod draw_editor;
77
mod draw_filepicker;
88
mod draw_menubar;
99
mod draw_statusbar;
10+
#[cfg(test)]
11+
mod layout_tests;
1012
mod localization;
1113
mod settings;
1214
mod state;
@@ -341,6 +343,9 @@ fn print_version() {
341343

342344
fn draw(tui: &mut Tui, input: Option<input::Input>, state: &mut State) {
343345
let ctx = &mut tui.create_context(input);
346+
ctx.attr_display(Display::Grid);
347+
ctx.attr_grid_template_columns(&[GridTrack::Fraction(1)]);
348+
ctx.attr_grid_template_rows(&[GridTrack::Auto, GridTrack::Fraction(1), GridTrack::Auto]);
344349

345350
draw_menubar(ctx, state);
346351
draw_editor(ctx, state);

0 commit comments

Comments
 (0)