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;
|
block.size.height -= self.room;
|
||||||
}
|
}
|
||||||
let view = self.list.viewport_bounds();
|
let view = self.list.viewport_bounds();
|
||||||
// A new layout or a resize moves it too.
|
// A new layout, a resize or the ends' room moves it.
|
||||||
let heights =
|
let heights = [block.size.height, view.size.height, self.room]
|
||||||
[block.size.height, view.size.height].map(|height| f32::from(height).round() as u64);
|
.map(|height| f32::from(height).round() as u64);
|
||||||
let moved = (ix, self.edits, self.typed, heights);
|
let moved = (ix, self.edits, self.typed, heights);
|
||||||
if self.centered == Some(moved) {
|
if self.centered == Some(moved) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ pub struct MarkdownDocument {
|
|||||||
/// Typewriter room past each end of the list.
|
/// Typewriter room past each end of the list.
|
||||||
room: Pixels,
|
room: Pixels,
|
||||||
/// The block, text, typing and heights last centered.
|
/// The block, text, typing and heights last centered.
|
||||||
centered: Option<(usize, u64, u64, [u64; 2])>,
|
centered: Option<(usize, u64, u64, [u64; 3])>,
|
||||||
focus: FocusHandle,
|
focus: FocusHandle,
|
||||||
/// A secondary press took focus.
|
/// A secondary press took focus.
|
||||||
pointing: bool,
|
pointing: bool,
|
||||||
|
|||||||
@@ -374,6 +374,7 @@ mod history;
|
|||||||
mod lend;
|
mod lend;
|
||||||
mod links;
|
mod links;
|
||||||
mod look;
|
mod look;
|
||||||
|
mod open_look;
|
||||||
mod place;
|
mod place;
|
||||||
mod point;
|
mod point;
|
||||||
mod presses;
|
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,
|
AnyElement, App, ClickEvent, Context, ExternalPaths, FontWeight, InteractiveElement,
|
||||||
IntoElement, KeyBinding, MouseButton, MouseDownEvent, ParentElement, Render,
|
IntoElement, KeyBinding, MouseButton, MouseDownEvent, ParentElement, Render,
|
||||||
StatefulInteractiveElement, Styled, StyledText, Window, actions, div, list,
|
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::{
|
use crate::{
|
||||||
documents::{
|
documents::{
|
||||||
MarkdownMode,
|
MarkdownMode, TargetBlock,
|
||||||
render::{Hooks, OpenCell, Pointed, draw_nodes, point_lines},
|
render::{HEAVY, Hooks, OpenCell, Pointed, draw_nodes, heading_step, point_lines},
|
||||||
},
|
},
|
||||||
forms::{self, Down, Up},
|
forms::{self, Down, Up},
|
||||||
primitives::FocusRing as _,
|
primitives::FocusRing as _,
|
||||||
@@ -72,22 +72,9 @@ impl MarkdownDocument {
|
|||||||
// Focus fades all but the reader's block.
|
// Focus fades all but the reader's block.
|
||||||
let faded = self.look.focus && editing_mode(self.mode) && ix != self.current_block(cx);
|
let faded = self.look.focus && editing_mode(self.mode) && ix != self.current_block(cx);
|
||||||
let open = self.open.as_ref();
|
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 {
|
let Some(block) = self.parsed.blocks.get(ix) else {
|
||||||
return match (open, editing) {
|
return match (open, editing) {
|
||||||
(Some(open), _) if open.append => field(open),
|
(Some(open), _) if open.append => self.field(open, None, cx),
|
||||||
(_, true) => self
|
(_, true) => self
|
||||||
.pressable(div().id("more").min_h_8(), None, cx)
|
.pressable(div().id("more").min_h_8(), None, cx)
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
@@ -95,7 +82,7 @@ impl MarkdownDocument {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
if let Some(open) = open.filter(|open| open.cell.is_none() && open.range == block.range) {
|
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.
|
// A cell open in this block, typed in place.
|
||||||
let cell = open.and_then(|open| {
|
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.
|
/// A right press in the open field points there.
|
||||||
fn point_in_open(
|
fn point_in_open(
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
|
|||||||
+14
-10
@@ -102,7 +102,18 @@ pub(crate) struct Hooks<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Headings and strong text share one weight.
|
/// 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.
|
/// How one inline style draws.
|
||||||
fn highlight(style: &Style, colors: &Palette, rule: Pixels) -> HighlightStyle {
|
fn highlight(style: &Style, colors: &Palette, rule: Pixels) -> HighlightStyle {
|
||||||
@@ -234,15 +245,8 @@ impl Draw<'_> {
|
|||||||
.child(self.inline(inline, cx))
|
.child(self.inline(inline, cx))
|
||||||
.into_any_element(),
|
.into_any_element(),
|
||||||
Node::Heading(level, inline) => {
|
Node::Heading(level, inline) => {
|
||||||
// Typora's steps over the body; every level bold.
|
// Every level bold.
|
||||||
let step = match level {
|
let size = theme.text_size(self.body) * heading_step(*level) * self.scale;
|
||||||
1 => 2.25,
|
|
||||||
2 => 1.75,
|
|
||||||
3 => 1.5,
|
|
||||||
4 => 1.25,
|
|
||||||
_ => 1.0,
|
|
||||||
};
|
|
||||||
let size = theme.text_size(self.body) * step * self.scale;
|
|
||||||
let ordinal = self.headings.get();
|
let ordinal = self.headings.get();
|
||||||
self.headings.set(ordinal + 1);
|
self.headings.set(ordinal + 1);
|
||||||
let heading = div()
|
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);
|
let (editor, cx) = editor("(a)", cx);
|
||||||
cx.run_until_parked();
|
cx.run_until_parked();
|
||||||
editor.update_in(cx, |editor, window, cx| {
|
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);
|
editor.replace_and_mark_text_in_range(None, "测", None, window, cx);
|
||||||
let text = editor.row_text(0).to_string();
|
let text = editor.row_text(0).to_string();
|
||||||
for (range, _) in editor.styles(0, cx) {
|
for (range, _) in editor.styles(0, cx) {
|
||||||
|
|||||||
Reference in new issue
Block a user