Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions crates/edit/src/bin/edit/draw_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,6 @@ pub fn draw_editor(ctx: &mut Context, state: &mut State) {
draw_search(ctx, state);
}

let size = ctx.size();
// TODO: The layout code should be able to just figure out the height on its own.
let height_reduction = match state.wants_search.kind {
StateSearchKind::Search => 4,
StateSearchKind::Replace => 5,
_ => 2,
};

if let Some(doc) = state.documents.active() {
ctx.textarea("textarea", doc.buffer.clone());
ctx.inherit_focus();
Expand All @@ -32,7 +24,7 @@ pub fn draw_editor(ctx: &mut Context, state: &mut State) {
ctx.block_end();
}

ctx.attr_intrinsic_size(Size { width: 0, height: size.height - height_reduction });
ctx.attr_fill_height();
}

fn draw_search(ctx: &mut Context, state: &mut State) {
Expand Down
32 changes: 31 additions & 1 deletion crates/edit/src/tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ use crate::input::{InputKeyMod, kbmod, vk};
use crate::oklab::StraightRgba;
use crate::{input, simd, unicode};

#[cfg(test)]
mod tests;

const ROOT_ID: u64 = 0x14057B7EF767814F; // Knuth's MMIX constant
const SHIFT_TAB: InputKey = vk::TAB.with_modifiers(kbmod::SHIFT);
const KBMOD_FOR_WORD_NAV: InputKeyMod =
Expand Down Expand Up @@ -1656,6 +1659,14 @@ impl<'a> Context<'a, '_> {
last_node.intrinsic_size_set = true;
}

/// In a vertical block, replaces the node's intrinsic height with an equal
/// share of the height remaining after fixed siblings, padding and borders.
/// Later filling siblings receive any remainder. Intrinsic measurement and
/// the specialized layout of tables and scrollareas are unchanged.
pub fn attr_fill_height(&mut self) {
self.tree.last_node.borrow_mut().attributes.fill_height = true;
}

/// Turns the current node into a floating node,
/// like a popup, modal or a tooltip.
pub fn attr_float(&mut self, spec: FloatSpec) {
Expand Down Expand Up @@ -3729,6 +3740,7 @@ struct FloatAttributes {
struct NodeAttributes {
float: Option<FloatAttributes>,
position: Position,
fill_height: bool,
padding: Rect,
bg: StraightRgba,
fg: StraightRgba,
Expand Down Expand Up @@ -4080,10 +4092,28 @@ impl<'a> Node<'a> {
let width = self.inner.right - self.inner.left;
let x = self.inner.left;
let mut y = self.inner.top;
let mut remaining_height = self.inner.bottom - self.inner.top;
let mut fill_count = 0;

for child in Tree::iterate_siblings(self.children.first) {
let child = child.borrow();
remaining_height -= child.intrinsic_to_outer().height;
if child.attributes.fill_height {
remaining_height += child.intrinsic_size.height;
fill_count += 1;
}
}
remaining_height = remaining_height.max(0);

for child in Tree::iterate_siblings(self.children.first) {
let mut child = child.borrow_mut();
let size = child.intrinsic_to_outer();
let mut size = child.intrinsic_to_outer();
if child.attributes.fill_height {
let height = remaining_height / fill_count;
size.height += height - child.intrinsic_size.height;
remaining_height -= height;
fill_count -= 1;
}
let remaining = (width - size.width).max(0);

child.outer.left = x + match child.attributes.position {
Expand Down
128 changes: 128 additions & 0 deletions crates/edit/src/tui/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use super::*;

fn outer(tui: &Tui, classname: &str) -> Rect {
let mut result = None;
let root = tui.prev_tree.root_first;
Tree::visit_all(root, root, true, |node| {
let node = node.borrow();
if node.classname == classname {
result = Some(node.outer);
}
VisitControl::Continue
});
result.unwrap()
}

fn block(ctx: &mut Context, classname: &'static str, height: CoordType, fill: bool) {
ctx.block_begin(classname);
ctx.attr_intrinsic_size(Size { width: 0, height });
if fill {
ctx.attr_fill_height();
}
ctx.block_end();
}

#[test]
fn fill_height_tracks_siblings_and_viewport() {
let mut tui = Tui::new().unwrap();
let empty = TextBuffer::new_rc(false).unwrap();
let long = TextBuffer::new_rc(false).unwrap();
long.borrow_mut().write_raw(&b"line\n".repeat(100));
// Hidden/disabled, search, replace, and a different surrounding layout.
for (header, search, footer) in [(1, 0, 1), (1, 2, 1), (1, 3, 1), (2, 7, 3)] {
for height in (1..=8).rev().chain(1..=8).chain([24, 80, 32767]) {
for buffer in [None, Some(&empty), Some(&long)] {
let classname = if buffer.is_some() { "textarea" } else { "empty" };
{
let mut ctx =
tui.create_context(Some(Input::Resize(Size { width: 80, height })));
block(&mut ctx, "header", header, false);
if search != 0 {
block(&mut ctx, "search", search, false);
}
if let Some(buffer) = buffer {
ctx.textarea(classname, buffer.clone());
ctx.attr_fill_height();
} else {
block(&mut ctx, classname, 0, true);
}
block(&mut ctx, "footer", footer, false);
block(&mut ctx, "popup", 50, false);
ctx.attr_float(FloatSpec { anchor: Anchor::Root, ..Default::default() });
}
let editor = outer(&tui, classname);
let expected = (height - header - search - footer).max(0);
assert_eq!(editor.bottom - editor.top, expected, "viewport {height}");
if height >= header + search + footer {
assert_eq!(editor.top, header + search);
assert_eq!(outer(&tui, "footer").top, height - footer);
}
}
}
}
}

#[test]
fn fill_height_handles_zero_and_coordinate_limit() {
// These geometry bounds are outside the terminal Resize event contract.
for height in [0, COORD_TYPE_SAFE_MAX] {
let mut tui = Tui::new().unwrap();
tui.set_size(Size { width: 80, height });
{
let mut ctx = tui.create_context(None);
block(&mut ctx, "header", 1, false);
block(&mut ctx, "editor", 100, true);
block(&mut ctx, "footer", 1, false);
}
let editor = outer(&tui, "editor");
assert_eq!(editor.bottom - editor.top, (height - 2).max(0));
}
}

#[test]
fn fill_height_shares_space_without_changing_fixed_layout() {
let mut tui = Tui::new().unwrap();
tui.set_size(Size { width: 20, height: 12 });
for fill in [false, true] {
{
let mut ctx = tui.create_context(None);
block(&mut ctx, "first", 2, fill);
block(&mut ctx, "middle", 3, false);
block(&mut ctx, "last", 1, fill);
}
let first = outer(&tui, "first");
let middle = outer(&tui, "middle");
let last = outer(&tui, "last");
assert_eq!(first.bottom - first.top, if fill { 4 } else { 2 });
assert_eq!(middle.top, first.bottom);
assert_eq!(middle.bottom - middle.top, 3);
assert_eq!(last.top, middle.bottom);
assert_eq!(last.bottom - last.top, if fill { 5 } else { 1 });
}
}

#[test]
fn fill_height_accounts_for_nested_padding_and_borders() {
let mut tui = Tui::new().unwrap();
tui.set_size(Size { width: 20, height: 20 });
{
let mut ctx = tui.create_context(None);
block(&mut ctx, "header", 2, false);
ctx.block_begin("container");
ctx.attr_fill_height();
ctx.attr_border();
ctx.attr_padding(Rect::two(1, 1));
block(&mut ctx, "content", 100, true);
ctx.attr_border();
block(&mut ctx, "footer", 1, false);
ctx.block_end();
block(&mut ctx, "status", 1, false);
}
assert_eq!(outer(&tui, "container"), Rect { left: 0, top: 2, right: 20, bottom: 19 });
assert_eq!(outer(&tui, "content"), Rect { left: 2, top: 4, right: 18, bottom: 16 });
assert_eq!(outer(&tui, "footer").top, 16);
assert_eq!(outer(&tui, "status").top, 19);
}