Documents: an open block keeps its look
CI / platforms (ubuntu-24.04) (push) Waiting to run
CI / platforms (windows-latest) (push) Failing after 2s
CI / crate (push) Failing after 2s
CI / site (push) Successful in 24s

The open field sits in the closed block's frame at its size, line
height and weight; headings keep their step. Code and math keep their
panel. Typewriter centering also keys on the ends' room, so a block
opened at the same height still centers.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-16 14:01:58 -04:00
1 parent cdc5ce8247
commit bff7ebcd64
7 files changed
+150 -34

No files matched your search

+3 -3
View File
@@ -90,9 +90,9 @@ impl MarkdownDocument {
block.size.height -= self.room;
}
let view = self.list.viewport_bounds();
// A new layout or a resize moves it too.
let heights =
[block.size.height, view.size.height].map(|height| f32::from(height).round() as u64);
// A new layout, a resize or the ends' room moves it.
let heights = [block.size.height, view.size.height, self.room]
.map(|height| f32::from(height).round() as u64);
let moved = (ix, self.edits, self.typed, heights);
if self.centered == Some(moved) {
return;
+1 -1
View File
@@ -117,7 +117,7 @@ pub struct MarkdownDocument {
/// Typewriter room past each end of the list.
room: Pixels,
/// The block, text, typing and heights last centered.
centered: Option<(usize, u64, u64, [u64; 2])>,
centered: Option<(usize, u64, u64, [u64; 3])>,
focus: FocusHandle,
/// A secondary press took focus.
pointing: bool,
+1
View File
@@ -374,6 +374,7 @@ mod history;
mod lend;
mod links;
mod look;
mod open_look;
mod place;
mod point;
mod presses;
+73
View File
@@ -0,0 +1,73 @@
//! An open block keeps its place and size (R7.5).
use gpui::{Bounds, Modifiers, Pixels, TestAppContext, VisualTestContext, rems};
use super::{open, press};
use crate::documents::MarkdownMode;
/// Block 0's bounds closed, then open.
fn closed_then_open<'a>(
source: &str,
cx: &'a mut TestAppContext,
) -> (Bounds<Pixels>, Bounds<Pixels>, &'a mut VisualTestContext) {
let (_, cx) = open(source, MarkdownMode::Visual, cx);
let closed = cx.debug_bounds("markdown-block-0").expect("drawn");
press(0, Modifiers::none(), cx);
let open = cx.debug_bounds("markdown-open").expect("the block opens");
(closed, open, cx)
}
/// Within a pixel: a field rounds its line height.
fn near(a: Pixels, b: Pixels) -> bool {
(a - b).abs() < gpui::px(1.0)
}
#[gpui::test]
fn an_open_paragraph_keeps_its_place_and_height(cx: &mut TestAppContext) {
let (closed, open, _) = closed_then_open("Plain words here.\n", cx);
assert!(
near(closed.top(), open.top()) && near(closed.left(), open.left()),
"{closed:?} {open:?}"
);
assert!(
near(closed.size.height, open.size.height),
"{closed:?} {open:?}"
);
}
#[gpui::test]
fn an_open_heading_keeps_its_size(cx: &mut TestAppContext) {
let (closed, open, _) = closed_then_open("## Plain heading\n", cx);
assert!(
near(closed.top(), open.top()) && near(closed.left(), open.left()),
"{closed:?} {open:?}"
);
assert!(
near(closed.size.height, open.size.height),
"{closed:?} {open:?}"
);
}
#[gpui::test]
fn an_open_code_block_keeps_its_panel(cx: &mut TestAppContext) {
let (closed, open, cx) = closed_then_open("```\nlet x = 1;\n```\n", cx);
assert!(
near(closed.top(), open.top()) && near(closed.left(), open.left()),
"{closed:?} {open:?}"
);
// The panel sits inside the frame's border and padding.
let (x, y) = cx.update(|window, _| {
let rem = window.rem_size();
(rems(0.75).to_pixels(rem), rems(0.25).to_pixels(rem))
});
let panel = cx.debug_bounds("markdown-open-text").expect("drawn");
let border = gpui::px(1.0);
assert!(
near(panel.left(), closed.left() + border + x),
"{panel:?} {closed:?}"
);
assert!(
near(panel.top(), closed.top() + border + y),
"{panel:?} {closed:?}"
);
}
+56 -19
View File
@@ -2,14 +2,14 @@ use gpui::{
AnyElement, App, ClickEvent, Context, ExternalPaths, FontWeight, InteractiveElement,
IntoElement, KeyBinding, MouseButton, MouseDownEvent, ParentElement, Render,
StatefulInteractiveElement, Styled, StyledText, Window, actions, div, list,
prelude::FluentBuilder as _,
prelude::FluentBuilder as _, relative,
};
use super::{MarkdownDocument, edit::Landing};
use super::{MarkdownDocument, edit::Landing, parse::Block};
use crate::{
documents::{
MarkdownMode,
render::{Hooks, OpenCell, Pointed, draw_nodes, point_lines},
MarkdownMode, TargetBlock,
render::{HEAVY, Hooks, OpenCell, Pointed, draw_nodes, heading_step, point_lines},
},
forms::{self, Down, Up},
primitives::FocusRing as _,
@@ -72,22 +72,9 @@ impl MarkdownDocument {
// Focus fades all but the reader's block.
let faded = self.look.focus && editing_mode(self.mode) && ix != self.current_block(cx);
let open = self.open.as_ref();
let point = Self::point_in_open(cx);
let field = |open: &super::Open| {
div()
.debug_selector(|| "markdown-open".to_string())
.on_mouse_down(MouseButton::Right, point)
.text_size(body)
.px_3()
.py_2()
.rounded(radius)
.bg(colors.sunken)
.child(open.input.clone())
.into_any_element()
};
let Some(block) = self.parsed.blocks.get(ix) else {
return match (open, editing) {
(Some(open), _) if open.append => field(open),
(Some(open), _) if open.append => self.field(open, None, cx),
(_, true) => self
.pressable(div().id("more").min_h_8(), None, cx)
.into_any_element(),
@@ -95,7 +82,7 @@ impl MarkdownDocument {
};
};
if let Some(open) = open.filter(|open| open.cell.is_none() && open.range == block.range) {
return field(open);
return self.field(open, Some(block), cx);
}
// A cell open in this block, typed in place.
let cell = open.and_then(|open| {
@@ -168,6 +155,56 @@ impl MarkdownDocument {
}
}
/// The open block in its closed frame, size and weight.
fn field(
&self,
open: &super::Open,
block: Option<&Block>,
cx: &mut Context<Self>,
) -> AnyElement {
let point = Self::point_in_open(cx);
let theme = cx.theme();
let colors = theme.colors.clone();
let body = theme.text_size(TextSize::Base) * self.look.scale;
let text = div()
.debug_selector(|| "markdown-open-text".to_string())
.text_size(body)
.line_height(body * 1.6)
.text_color(colors.fg);
let text = match block.map(|block| (block.kind, block.nodes.is_some())) {
Some((TargetBlock::Heading(level), true)) => text
.pt_2()
.text_size(body * heading_step(level))
.line_height(relative(1.3))
.font_weight(HEAVY)
.when(level == 6, |text| text.text_color(colors.fg_muted)),
// Code and math keep their panel.
Some((kind @ (TargetBlock::Code | TargetBlock::Math), true)) => text
.p_3()
.rounded(theme.radius(Radius::Md))
.bg(colors.sunken)
.when(kind == TargetBlock::Code, |text| {
text.font_family(theme.mono_family.clone())
.text_size(theme.text_size(TextSize::Sm) * self.look.scale)
}),
// Written as is: mono and muted.
Some((_, false)) => text
.font_family(theme.mono_family.clone())
.line_height(gpui::phi())
.text_color(colors.fg_muted),
_ => text,
};
div()
.debug_selector(|| "markdown-open".to_string())
.on_mouse_down(MouseButton::Right, point)
.px_3()
.py_1()
.border_1()
.border_color(gpui::transparent_black())
.child(text.child(open.input.clone()))
.into_any_element()
}
/// A right press in the open field points there.
fn point_in_open(
cx: &mut Context<Self>,
+14 -10
View File
@@ -102,7 +102,18 @@ pub(crate) struct Hooks<'a> {
}
/// Headings and strong text share one weight.
const HEAVY: FontWeight = FontWeight::BOLD;
pub(crate) const HEAVY: FontWeight = FontWeight::BOLD;
/// Typora's heading steps over the body.
pub(crate) fn heading_step(level: u8) -> f32 {
match level {
1 => 2.25,
2 => 1.75,
3 => 1.5,
4 => 1.25,
_ => 1.0,
}
}
/// How one inline style draws.
fn highlight(style: &Style, colors: &Palette, rule: Pixels) -> HighlightStyle {
@@ -234,15 +245,8 @@ impl Draw<'_> {
.child(self.inline(inline, cx))
.into_any_element(),
Node::Heading(level, inline) => {
// Typora's steps over the body; every level bold.
let step = match level {
1 => 2.25,
2 => 1.75,
3 => 1.5,
4 => 1.25,
_ => 1.0,
};
let size = theme.text_size(self.body) * step * self.scale;
// Every level bold.
let size = theme.text_size(self.body) * heading_step(*level) * self.scale;
let ordinal = self.headings.get();
self.headings.set(ordinal + 1);
let heading = div()
+2 -1
View File
@@ -52,7 +52,8 @@ fn an_input_method_edit_measures_before_the_next_draw(cx: &mut TestAppContext) {
let (editor, cx) = editor("(a)", cx);
cx.run_until_parked();
editor.update_in(cx, |editor, window, cx| {
editor.select([0..0], cx);
let start = 0..0;
editor.select([start], cx);
editor.replace_and_mark_text_in_range(None, "测", None, window, cx);
let text = editor.row_text(0).to_string();
for (range, _) in editor.styles(0, cx) {