T21a: Documents, editing

- markdown read with pulldown-cmark: marks for live styling, blocks, links, words
- forms::Highlight gains weight, slant and strike; measure and paint share runs
- Inter Italic embedded, so emphasis reads slanted
- forms::mention's popup and keys become Suggestions, shared with the editors
- RichTextEditor with FixedFormatToolbar, the floating toolbar, LinkEditor,
  @ and : suggestions, WordCount and ReadingTime
- MarkdownEditor: visual (a pressed block opens as source), source, split
- BlockEditor: prose, lists, to-dos, quotes, callouts, toggles, code, math,
  diagrams the app draws, dividers, tables that grow and merge, pictures to
  resize and align, cards for video and pages, columns, synced text; slash menu,
  markdown shortcuts, handles that drag and open a menu, one undo for all
- TodoItem and Checklist
- icons: heading-1..3, workflow, merge
- gallery: the Documents page with scripted states
This commit is contained in:
ZacharyZhang-NY committed 2026-09-26 03:32:24 -04:00
1 parent 05e764304d
commit 9edd0a8612
46 files changed
+5635 -193

No files matched your search

+105 -49
View File
@@ -1,11 +1,11 @@
use gpui::{
App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Element, ElementId,
ElementInputHandler, Entity, GlobalElementId, Hsla, InspectorElementId, IntoElement, LayoutId,
MouseMoveEvent, Pixels, Point, SharedString, Style, TextAlign, TextRun, UnderlineStyle, Window,
WrappedLine, fill, point, relative, size,
ElementInputHandler, Entity, FontStyle, GlobalElementId, Hsla, InspectorElementId, IntoElement,
LayoutId, MouseMoveEvent, Pixels, Point, SharedString, StrikethroughStyle, Style, TextAlign,
TextRun, UnderlineStyle, Window, WrappedLine, fill, point, relative, size,
};
use super::{Layout, TextInput};
use super::{Highlight, Layout, TextInput};
use crate::theme::ActiveTheme;
pub(crate) struct TextElement {
@@ -54,11 +54,11 @@ fn plain_run(len: usize, window: &Window, color: Hsla) -> TextRun {
}
}
/// Splits `len` bytes into runs: highlights and the marked range over a base color.
/// Splits `len` bytes into runs: highlights, and the underlined marked range, over a base run.
fn runs(
len: usize,
base: TextRun,
spans: Vec<(std::ops::Range<usize>, Hsla, Option<Hsla>, bool)>,
spans: Vec<(std::ops::Range<usize>, Highlight, bool)>,
thickness: Pixels,
) -> Vec<TextRun> {
let mut cuts: Vec<usize> = vec![0, len];
@@ -74,15 +74,27 @@ fn runs(
len: end - start,
..base.clone()
};
for (range, color, background, underline) in &spans {
for (range, highlight, underline) in &spans {
if range.start <= start && end <= range.end {
run.color = *color;
if background.is_some() {
run.background_color = *background;
run.color = highlight.color;
if highlight.background.is_some() {
run.background_color = highlight.background;
}
if let Some(weight) = highlight.weight {
run.font.weight = weight;
}
if highlight.italic {
run.font.style = FontStyle::Italic;
}
if highlight.strike {
run.strikethrough = Some(StrikethroughStyle {
thickness,
color: Some(highlight.color),
});
}
if *underline {
run.underline = Some(UnderlineStyle {
color: Some(*color),
color: Some(highlight.color),
thickness,
wavy: false,
});
@@ -95,6 +107,34 @@ fn runs(
.collect()
}
/// What to show, whether it is the placeholder, and its runs, the same for measure and paint.
fn shown_runs(input: &TextInput, window: &Window, cx: &App) -> (String, bool, Vec<TextRun>) {
let theme = cx.theme();
let (text, placeholder) = shown(input);
let color = window.text_style().color;
let base = plain_run(
text.len(),
window,
if placeholder {
theme.colors.fg_subtle
} else {
color
},
);
let mut spans = Vec::new();
if !placeholder {
for (range, highlight) in input.highlights(cx) {
spans.push((range, highlight, false));
}
if let Some(marked) = input.marked() {
let marked = input.display_offset(marked.start)..input.display_offset(marked.end);
spans.push((marked, Highlight::new(color), true));
}
}
let runs = runs(text.len(), base, spans, theme.underline_thickness());
(text, placeholder, runs)
}
/// Starts of the hard lines in `text`.
fn line_starts(text: &str) -> Vec<usize> {
std::iter::once(0)
@@ -129,9 +169,8 @@ impl Element for TextElement {
style.size.height = line_height.into();
return (window.request_layout(style, [], cx), ());
};
let (text, _) = shown(input);
let (text, _, runs) = shown_runs(input, window, cx);
let font_size = window.text_style().font_size.to_pixels(window.rem_size());
let run = plain_run(text.len(), window, Hsla::default());
let layout = window.request_measured_layout(style, move |known, available, window, _| {
let width = known.width.or(match available.width {
AvailableSpace::Definite(width) => Some(width),
@@ -142,7 +181,7 @@ impl Element for TextElement {
.shape_text(
SharedString::from(text.clone()),
font_size,
std::slice::from_ref(&run),
&runs,
width,
None,
)
@@ -169,47 +208,16 @@ impl Element for TextElement {
cx: &mut App,
) -> Prepaint {
let theme = cx.theme();
let (selection_color, caret_color, subtle) = (
theme.colors.selection,
theme.colors.focus,
theme.colors.fg_subtle,
);
let (selection_color, caret_color) = (theme.colors.selection, theme.colors.focus);
let caret_width = theme.caret_width().to_pixels(window.rem_size());
let thickness = theme.underline_thickness();
let input = self.input.read(cx);
let (text, placeholder) = shown(input);
let base = plain_run(
text.len(),
window,
if placeholder {
subtle
} else {
window.text_style().color
},
);
let mut spans = Vec::new();
if !placeholder {
for (range, highlight) in input.highlights(cx) {
spans.push((range, highlight.color, highlight.background, false));
}
if let Some(marked) = input.marked() {
let color = window.text_style().color;
let marked = input.display_offset(marked.start)..input.display_offset(marked.end);
spans.push((marked, color, None, true));
}
}
let (text, placeholder, runs) = shown_runs(input, window, cx);
let font_size = window.text_style().font_size.to_pixels(window.rem_size());
let line_height = window.line_height();
let wrap = input.rows().map(|_| bounds.size.width);
let shaped = window
.text_system()
.shape_text(
text.clone().into(),
font_size,
&runs(text.len(), base, spans, thickness),
wrap,
None,
)
.shape_text(text.clone().into(), font_size, &runs, wrap, None)
.expect("text input shaping failed");
let lines: Vec<(usize, WrappedLine)> = line_starts(&text).into_iter().zip(shaped).collect();
let focused = input.focus().is_focused(window);
@@ -400,7 +408,55 @@ fn selection_rects(
#[cfg(test)]
mod tests {
use super::line_starts;
use gpui::{FontWeight, font, hsla, px};
use super::*;
#[test]
fn a_highlight_sets_weight_slant_and_strike_on_its_runs() {
let base = TextRun {
len: 3,
font: font("Inter"),
color: hsla(0., 0., 0., 1.),
background_color: None,
underline: None,
strikethrough: None,
};
let bold = Highlight {
weight: Some(FontWeight::SEMIBOLD),
..Highlight::new(hsla(0., 0., 0.5, 1.))
};
let slanted = Highlight {
italic: true,
strike: true,
..Highlight::new(hsla(0., 0., 0.2, 1.))
};
let split = runs(
3,
base,
vec![(0..2, bold, false), (1..3, slanted, false)],
px(1.),
);
let styles: Vec<_> = split
.iter()
.map(|run| {
(
run.len,
run.font.weight,
run.font.style,
run.strikethrough.is_some(),
)
})
.collect();
assert_eq!(
styles,
[
(1, FontWeight::SEMIBOLD, FontStyle::Normal, false),
(1, FontWeight::SEMIBOLD, FontStyle::Italic, true),
(1, FontWeight::NORMAL, FontStyle::Italic, true),
]
);
}
#[test]
fn line_starts_follow_newlines() {
+24
View File
@@ -0,0 +1,24 @@
use gpui::{FontWeight, Hsla};
/// Styles a span of the text: its color, an optional wash behind it, and its weight, slant and strike.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Highlight {
pub color: Hsla,
pub background: Option<Hsla>,
pub weight: Option<FontWeight>,
pub italic: bool,
pub strike: bool,
}
impl Highlight {
/// A span in `color`, its font as the field's.
pub fn new(color: Hsla) -> Self {
Self {
color,
background: None,
weight: None,
italic: false,
strike: false,
}
}
}
+4 -9
View File
@@ -1,18 +1,20 @@
mod actions;
mod edit;
mod element;
mod highlight;
mod ime;
use std::{ops::Range, rc::Rc, time::Duration};
use gpui::{
App, Bounds, Context, EventEmitter, FocusHandle, Focusable, Hsla, Pixels, Point, SharedString,
App, Bounds, Context, EventEmitter, FocusHandle, Focusable, Pixels, Point, SharedString,
Subscription, Task, Window, WrappedLine,
};
pub(crate) use actions::{Backspace, Down, Enter, Submit, Up, bind_keys};
pub(crate) use actions::{Backspace, Down, Enter, Redo, Submit, Undo, Up, bind_keys};
use edit::Snapshot;
pub(crate) use edit::{History, from_utf16, to_utf16};
pub use highlight::Highlight;
use crate::theme::ActiveTheme;
@@ -26,13 +28,6 @@ pub enum InputEvent {
Blur,
}
/// Colors a span of the text: foreground, and an optional wash behind it.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Highlight {
pub color: Hsla,
pub background: Option<Hsla>,
}
type Highlighter = Rc<dyn Fn(&str, &App) -> Vec<(Range<usize>, Highlight)>>;
type Filter = Rc<dyn Fn(char) -> bool>;
pub(crate) type Fit = Rc<dyn Fn(&str, Range<usize>, &str) -> (String, usize)>;