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:
46 files changed
+5635
-193
No files matched your search
+1
-7
@@ -83,13 +83,7 @@ pub(crate) fn lex(source: &str) -> Vec<(Range<usize>, Kind)> {
|
||||
|
||||
fn painted(spans: impl Iterator<Item = (Range<usize>, Hsla)>) -> Vec<(Range<usize>, Highlight)> {
|
||||
spans
|
||||
.map(|(range, color)| {
|
||||
let highlight = Highlight {
|
||||
color,
|
||||
background: None,
|
||||
};
|
||||
(range, highlight)
|
||||
})
|
||||
.map(|(range, color)| (range, Highlight::new(color)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
||||
@@ -237,13 +237,7 @@ pub fn expression_highlights(source: &str, cx: &App) -> Vec<(Range<usize>, Highl
|
||||
Token::Op(_) => syntax.operator,
|
||||
Token::Open | Token::Close | Token::Comma => syntax.punctuation,
|
||||
};
|
||||
(
|
||||
range.clone(),
|
||||
Highlight {
|
||||
color,
|
||||
background: None,
|
||||
},
|
||||
)
|
||||
(range.clone(), Highlight::new(color))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
+1
-1
@@ -338,7 +338,7 @@ impl RenderOnce for IconPicker {
|
||||
}
|
||||
|
||||
/// Emoji whose name or shortcode holds `query`, a lowercase string; all for an empty one.
|
||||
fn emoji_found(query: &str) -> Vec<&'static emojis::Emoji> {
|
||||
pub(crate) fn emoji_found(query: &str) -> Vec<&'static emojis::Emoji> {
|
||||
emojis::iter()
|
||||
.filter(|emoji| {
|
||||
query.is_empty()
|
||||
|
||||
+114
-90
@@ -1,13 +1,13 @@
|
||||
use std::{ops::Range, rc::Rc};
|
||||
|
||||
use gpui::{
|
||||
App, ElementId, Entity, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
||||
SharedString, Styled, Window, div, prelude::*,
|
||||
App, Div, ElementId, Entity, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
||||
SharedString, Stateful, Styled, Window, div, prelude::*,
|
||||
};
|
||||
|
||||
use super::{
|
||||
Highlight, Input, TextInput,
|
||||
options::{Choice, Popup},
|
||||
options::{Choice, Pick, Popup},
|
||||
text::{Down, Enter, Up},
|
||||
};
|
||||
use crate::theme::ActiveTheme;
|
||||
@@ -72,8 +72,8 @@ pub fn mention_highlights(text: &str, cx: &App) -> Vec<(Range<usize>, Highlight)
|
||||
(
|
||||
range,
|
||||
Highlight {
|
||||
color: link,
|
||||
background: Some(link.opacity(0.08)),
|
||||
..Highlight::new(link)
|
||||
},
|
||||
)
|
||||
})
|
||||
@@ -86,6 +86,94 @@ struct Picking {
|
||||
dismissed: Option<usize>,
|
||||
}
|
||||
|
||||
/// Rows offered at a trigger in a field's text, and what a pick does: arrows choose, Enter or a press picks, Escape dismisses until the trigger ends.
|
||||
pub(crate) struct Suggestions {
|
||||
pub id: ElementId,
|
||||
pub state: Entity<TextInput>,
|
||||
/// Where the trigger being typed starts.
|
||||
pub trigger: Option<usize>,
|
||||
pub rows: Vec<Choice>,
|
||||
pub pick: Pick,
|
||||
}
|
||||
|
||||
impl Suggestions {
|
||||
/// Wraps the element that shows the field, with the rows under the trigger while it is typed.
|
||||
pub fn wrap(self, field: impl IntoElement, window: &mut Window, cx: &mut App) -> Stateful<Div> {
|
||||
let picking = window.use_keyed_state(self.id.clone(), cx, |_, _| Picking::default());
|
||||
let input = self.state.read(cx);
|
||||
let focused = input.focus().is_focused(window);
|
||||
let anchor = self.trigger.and_then(|ix| input.bounds_for(ix));
|
||||
let dismissed = dismissal(picking.read(cx).dismissed, self.trigger);
|
||||
if dismissed != picking.read(cx).dismissed {
|
||||
picking.update(cx, |picking, _| picking.dismissed = dismissed);
|
||||
}
|
||||
let open = focused
|
||||
&& !self.rows.is_empty()
|
||||
&& self.trigger.is_some_and(|ix| dismissed != Some(ix));
|
||||
let count = self.rows.len();
|
||||
let highlighted = picking.read(cx).highlighted.min(count.saturating_sub(1));
|
||||
log::debug!(
|
||||
"suggestions {:?}: open {open}, {count} rows, anchor {anchor:?}",
|
||||
self.id
|
||||
);
|
||||
let (up, down, escape) = (picking.clone(), picking.clone(), picking);
|
||||
let (enter, trigger) = (self.pick.clone(), self.trigger);
|
||||
div()
|
||||
.id(self.id)
|
||||
.relative()
|
||||
.w_full()
|
||||
.capture_action(move |_: &Up, _, cx| {
|
||||
if open {
|
||||
cx.stop_propagation();
|
||||
up.update(cx, |picking, cx| {
|
||||
picking.highlighted = (picking.highlighted + count - 1) % count;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
})
|
||||
.capture_action(move |_: &Down, _, cx| {
|
||||
if open {
|
||||
cx.stop_propagation();
|
||||
down.update(cx, |picking, cx| {
|
||||
picking.highlighted = (picking.highlighted + 1) % count;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
})
|
||||
.capture_action(move |_: &Enter, window, cx| {
|
||||
if open {
|
||||
cx.stop_propagation();
|
||||
enter(highlighted, window, cx);
|
||||
}
|
||||
})
|
||||
.on_key_down(move |event, _, cx| {
|
||||
if open && event.keystroke.key == "escape" {
|
||||
cx.stop_propagation();
|
||||
escape.update(cx, |picking, cx| {
|
||||
picking.dismissed = trigger;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
})
|
||||
.child(field)
|
||||
.when_some(anchor.filter(|_| open), |field, anchor| {
|
||||
field.child(
|
||||
Popup {
|
||||
id: "suggestions".into(),
|
||||
anchor,
|
||||
rows: &self.rows,
|
||||
highlighted: Some(highlighted),
|
||||
checked: None,
|
||||
pick: self.pick,
|
||||
dismiss: None,
|
||||
scroll: None,
|
||||
}
|
||||
.render(window, cx),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A field where `@` or `#` opens suggestions at the caret. Arrows choose; Enter picks.
|
||||
#[derive(IntoElement)]
|
||||
pub struct MentionInput {
|
||||
@@ -122,46 +210,34 @@ impl MentionInput {
|
||||
}
|
||||
}
|
||||
|
||||
/// The handles after `trigger` that hold the typed query, a few at most.
|
||||
pub(crate) fn handles_matching(handles: &[SharedString], query: &str) -> Vec<SharedString> {
|
||||
let query = query.to_lowercase();
|
||||
handles
|
||||
.iter()
|
||||
.filter(|name| name.to_lowercase().contains(&query))
|
||||
.take(SHOWN)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl RenderOnce for MentionInput {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let picking = window.use_keyed_state(self.id.clone(), cx, |_, _| Picking::default());
|
||||
let input = self.state.read(cx);
|
||||
let (text, caret) = (input.text().to_string(), input.cursor());
|
||||
let focused = input.focus().is_focused(window);
|
||||
let marks: Vec<char> = self.triggers.iter().map(|(trigger, _)| *trigger).collect();
|
||||
let active = active_trigger(&text, caret, &marks);
|
||||
let anchor = active.and_then(|(ix, _)| input.bounds_for(ix));
|
||||
let matches: Vec<SharedString> = active
|
||||
.map(|(ix, trigger)| {
|
||||
let query = text[ix + trigger.len_utf8()..caret].to_lowercase();
|
||||
let names = &self
|
||||
.triggers
|
||||
.iter()
|
||||
.find(|(mark, _)| *mark == trigger)
|
||||
.expect("from triggers")
|
||||
.1;
|
||||
names
|
||||
.iter()
|
||||
.filter(|name| name.to_lowercase().contains(&query))
|
||||
.take(SHOWN)
|
||||
.cloned()
|
||||
.collect()
|
||||
handles_matching(names, &text[ix + trigger.len_utf8()..caret])
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let dismissed = dismissal(picking.read(cx).dismissed, active.map(|(ix, _)| ix));
|
||||
if dismissed != picking.read(cx).dismissed {
|
||||
picking.update(cx, |picking, _| picking.dismissed = dismissed);
|
||||
}
|
||||
let open =
|
||||
focused && !matches.is_empty() && active.is_some_and(|(ix, _)| dismissed != Some(ix));
|
||||
let highlighted = picking
|
||||
.read(cx)
|
||||
.highlighted
|
||||
.min(matches.len().saturating_sub(1));
|
||||
log::debug!(
|
||||
"mention input: open {open}, {} matches, caret {anchor:?}",
|
||||
matches.len()
|
||||
);
|
||||
let rows: Vec<Choice> = matches
|
||||
.iter()
|
||||
.map(|name| {
|
||||
@@ -171,8 +247,8 @@ impl RenderOnce for MentionInput {
|
||||
Choice::new(name.clone(), label)
|
||||
})
|
||||
.collect();
|
||||
let insert = {
|
||||
let (state, matches) = (self.state.clone(), matches.clone());
|
||||
let insert: Pick = {
|
||||
let state = self.state.clone();
|
||||
Rc::new(move |pick: usize, _: &mut Window, cx: &mut App| {
|
||||
let Some((ix, trigger)) = active else {
|
||||
return;
|
||||
@@ -185,66 +261,14 @@ impl RenderOnce for MentionInput {
|
||||
});
|
||||
})
|
||||
};
|
||||
let count = matches.len();
|
||||
let (up, down, enter, escape) = (
|
||||
picking.clone(),
|
||||
picking.clone(),
|
||||
insert.clone(),
|
||||
picking.clone(),
|
||||
);
|
||||
div()
|
||||
.id(self.id)
|
||||
.relative()
|
||||
.w_full()
|
||||
.capture_action(move |_: &Up, _, cx| {
|
||||
if open {
|
||||
cx.stop_propagation();
|
||||
up.update(cx, |picking, cx| {
|
||||
picking.highlighted = (picking.highlighted + count - 1) % count;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
})
|
||||
.capture_action(move |_: &Down, _, cx| {
|
||||
if open {
|
||||
cx.stop_propagation();
|
||||
down.update(cx, |picking, cx| {
|
||||
picking.highlighted = (picking.highlighted + 1) % count;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
})
|
||||
.capture_action(move |_: &Enter, window, cx| {
|
||||
if open {
|
||||
cx.stop_propagation();
|
||||
enter(highlighted, window, cx);
|
||||
}
|
||||
})
|
||||
.on_key_down(move |event, _, cx| {
|
||||
if open && event.keystroke.key == "escape" {
|
||||
cx.stop_propagation();
|
||||
escape.update(cx, |picking, cx| {
|
||||
picking.dismissed = active.map(|(ix, _)| ix);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
})
|
||||
.child(Input::new(&self.state))
|
||||
.when_some(anchor.filter(|_| open), |field, anchor| {
|
||||
field.child(
|
||||
Popup {
|
||||
id: "mentions".into(),
|
||||
anchor,
|
||||
rows: &rows,
|
||||
highlighted: Some(highlighted),
|
||||
checked: None,
|
||||
pick: insert,
|
||||
dismiss: None,
|
||||
scroll: None,
|
||||
}
|
||||
.render(window, cx),
|
||||
)
|
||||
})
|
||||
Suggestions {
|
||||
id: self.id,
|
||||
state: self.state.clone(),
|
||||
trigger: active.map(|(ix, _)| ix),
|
||||
rows,
|
||||
pick: insert,
|
||||
}
|
||||
.wrap(Input::new(&self.state), window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -64,6 +64,7 @@ pub use date::{
|
||||
};
|
||||
pub use expression::{ExpressionInput, evaluate, expression_highlights};
|
||||
pub use files::{DropZone, FileInput};
|
||||
pub(crate) use glyphs::emoji_found;
|
||||
pub use glyphs::{EmojiPicker, IconPicker};
|
||||
pub use group::{InputAddon, InputGroup};
|
||||
pub use hotkey::HotkeyInput;
|
||||
@@ -74,6 +75,7 @@ pub use knob::Knob;
|
||||
pub use listbox::ListBox;
|
||||
pub use masked::{COUNTRIES, MaskedInput, PhoneInput};
|
||||
pub use mention::{MentionInput, mention_highlights};
|
||||
pub(crate) use mention::{Suggestions, active_trigger, handles_matching};
|
||||
pub use multi::MultiSelect;
|
||||
pub use number::{NumberInput, ScrubInput};
|
||||
pub use options::Choice;
|
||||
@@ -95,7 +97,9 @@ pub use structure::{
|
||||
};
|
||||
pub use switch::Switch;
|
||||
pub use tags::TagInput;
|
||||
pub(crate) use text::{Down, Enter, History, Submit, Up, bind_keys, from_utf16, to_utf16};
|
||||
pub(crate) use text::{
|
||||
Backspace, Down, Enter, History, Redo, Submit, Undo, Up, bind_keys, from_utf16, to_utf16,
|
||||
};
|
||||
pub use text::{Highlight, InputEvent, TextInput};
|
||||
pub use transfer::TransferList;
|
||||
pub use unit::UnitInput;
|
||||
|
||||
@@ -90,13 +90,7 @@ pub fn regex_highlights(pattern: &str, cx: &App) -> Vec<(Range<usize>, Highlight
|
||||
Part::Anchor => syntax.constant,
|
||||
Part::Alternation => syntax.operator,
|
||||
};
|
||||
(
|
||||
range,
|
||||
Highlight {
|
||||
color,
|
||||
background: None,
|
||||
},
|
||||
)
|
||||
(range, Highlight::new(color))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
+105
-49
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)>;
|
||||
|
||||
Reference in new issue
Block a user