Documents: an open block keeps its look
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:
1 parent
cdc5ce8247
commit
bff7ebcd64
7 files changed
+150
-34
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -374,6 +374,7 @@ mod history;
|
||||
mod lend;
|
||||
mod links;
|
||||
mod look;
|
||||
mod open_look;
|
||||
mod place;
|
||||
mod point;
|
||||
mod presses;
|
||||
|
||||
@@ -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:?}"
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in new issue
Block a user