theme::Platform moves out of typography and shell's ControlsStyle folds into it; the theme's platform starts as the system's. Window buttons Ely draws, the title bar's layout and every key cap follow it, secondary included, through typography::keys::keystroke; a title bar whose buttons the OS draws follows the OS. primitives::IconTheme lays an owner's rules by file name and extension over Ely's map, for every file icon. shell::Vibrancy fills a pane with the palette's new glass, translucent so a window opened Blurred shows its backdrop, solid under high contrast. gpui 0.2.2 has no Mica; that line stays blocked.
189 lines
6.5 KiB
Rust
189 lines
6.5 KiB
Rust
use std::rc::Rc;
|
|
|
|
use gpui::{
|
|
AnyElement, App, ElementId, Entity, InteractiveElement, IntoElement, KeyBinding, ParentElement,
|
|
RenderOnce, SharedString, Styled, Window, actions, div, prelude::*,
|
|
};
|
|
|
|
use super::{
|
|
counts::{ReadingTime, WordCount},
|
|
format::{Format, active, format},
|
|
link::LinkEditor,
|
|
suggest::offers,
|
|
toolbar::{FixedFormatToolbar, INLINE, tool},
|
|
};
|
|
use crate::{
|
|
forms::{Run, TextInput},
|
|
overlays::FloatingToolbar,
|
|
theme::{ActiveTheme, Radius, TextSize},
|
|
};
|
|
|
|
actions!(ely_rich_text, [ToggleBold, ToggleItalic, EditLink]);
|
|
|
|
const CONTEXT: &str = "ElyRichText";
|
|
|
|
/// Binds the rich text keys. `init` calls it.
|
|
pub(crate) fn bind_keys(cx: &mut App) {
|
|
cx.bind_keys([
|
|
KeyBinding::new("secondary-b", ToggleBold, Some(CONTEXT)),
|
|
KeyBinding::new("secondary-i", ToggleItalic, Some(CONTEXT)),
|
|
KeyBinding::new("secondary-k", EditLink, Some(CONTEXT)),
|
|
]);
|
|
}
|
|
|
|
/// Markdown that reads as it is written: styles show as they are typed, the toolbar above and the one over a selection apply them, a link opens its editor, `@` and `:` suggest people and emoji, and the foot counts words and minutes.
|
|
#[derive(IntoElement)]
|
|
pub struct RichTextEditor {
|
|
id: ElementId,
|
|
field: Entity<TextInput>,
|
|
people: Vec<SharedString>,
|
|
end: Option<AnyElement>,
|
|
beside: Option<AnyElement>,
|
|
}
|
|
|
|
impl RichTextEditor {
|
|
/// Give the field several lines and `markdown_highlights` as its highlighter.
|
|
pub fn new(id: impl Into<ElementId>, field: &Entity<TextInput>) -> Self {
|
|
Self {
|
|
id: id.into(),
|
|
field: field.clone(),
|
|
people: Vec::new(),
|
|
end: None,
|
|
beside: None,
|
|
}
|
|
}
|
|
|
|
/// Sits at the toolbar's far end, such as a switch of views.
|
|
pub fn toolbar_end(mut self, element: impl IntoElement) -> Self {
|
|
self.end = Some(element.into_any_element());
|
|
self
|
|
}
|
|
|
|
/// Sits beside the text, as a preview does.
|
|
pub fn beside(mut self, element: impl IntoElement) -> Self {
|
|
self.beside = Some(element.into_any_element());
|
|
self
|
|
}
|
|
|
|
/// Handles `@` suggests.
|
|
pub fn people(mut self, handles: impl IntoIterator<Item = impl Into<SharedString>>) -> Self {
|
|
self.people = handles.into_iter().map(Into::into).collect();
|
|
self
|
|
}
|
|
}
|
|
|
|
impl RenderOnce for RichTextEditor {
|
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
let input = self.field.read(cx);
|
|
assert!(
|
|
input.is_multi_line(),
|
|
"rich text {:?} needs a field of several lines",
|
|
self.id
|
|
);
|
|
let text = SharedString::from(input.text().to_string());
|
|
let pressed = active(input.text(), input.selection());
|
|
let linking = window.use_keyed_state((self.id.clone(), "linking"), cx, |_, _| false);
|
|
let set_linking = |open: bool| -> Run {
|
|
let linking = linking.clone();
|
|
Rc::new(move |_, cx| {
|
|
log::info!(
|
|
"rich text: link editor {}",
|
|
if open { "opens" } else { "closes" }
|
|
);
|
|
linking.update(cx, |linking, cx| {
|
|
*linking = open;
|
|
cx.notify();
|
|
});
|
|
})
|
|
};
|
|
let (open_link, close_link) = (set_linking(true), set_linking(false));
|
|
let float_id: ElementId = (self.id.clone(), "float").into();
|
|
let platform = cx.theme().platform;
|
|
let floating = FloatingToolbar::new(float_id.clone(), &self.field).children(
|
|
INLINE.iter().map(|tool_of| {
|
|
let on = pressed.contains(&tool_of.0);
|
|
tool(
|
|
&float_id,
|
|
&self.field,
|
|
*tool_of,
|
|
on,
|
|
Some(open_link.clone()),
|
|
platform,
|
|
)
|
|
}),
|
|
);
|
|
let suggestions = offers(
|
|
(self.id.clone(), "offers").into(),
|
|
&self.field,
|
|
&self.people,
|
|
None,
|
|
cx,
|
|
)
|
|
.wrap(self.field.clone(), window, cx);
|
|
let theme = cx.theme();
|
|
let colors = theme.colors.clone();
|
|
let (bold, italic, link) = (self.field.clone(), self.field.clone(), open_link.clone());
|
|
div()
|
|
.id(self.id.clone())
|
|
.key_context(CONTEXT)
|
|
.on_action(move |_: &ToggleBold, _, cx| format(&bold, Format::Bold, cx))
|
|
.on_action(move |_: &ToggleItalic, _, cx| format(&italic, Format::Italic, cx))
|
|
.on_action(move |_: &EditLink, window, cx| link(window, cx))
|
|
.flex()
|
|
.flex_col()
|
|
.rounded(theme.radius(Radius::Lg))
|
|
.border_1()
|
|
.border_color(colors.border)
|
|
.bg(colors.surface)
|
|
.child(
|
|
div()
|
|
.flex()
|
|
.items_center()
|
|
.justify_between()
|
|
.px_2()
|
|
.border_b_1()
|
|
.border_color(colors.border)
|
|
.child(
|
|
FixedFormatToolbar::new((self.id.clone(), "tools"), &self.field)
|
|
.on_link(move |window, cx| open_link(window, cx)),
|
|
)
|
|
.children(self.end),
|
|
)
|
|
.child(
|
|
div()
|
|
.flex()
|
|
.child(div().flex_1().min_w_0().px_4().py_3().child(suggestions))
|
|
.children(self.beside.map(|beside| {
|
|
div()
|
|
.flex_1()
|
|
.min_w_0()
|
|
.px_4()
|
|
.py_3()
|
|
.border_l_1()
|
|
.border_color(colors.border)
|
|
.child(beside)
|
|
})),
|
|
)
|
|
.child(floating)
|
|
.child(
|
|
div()
|
|
.flex()
|
|
.justify_between()
|
|
.px_4()
|
|
.py_2()
|
|
.border_t_1()
|
|
.border_color(colors.border)
|
|
.text_size(theme.text_size(TextSize::Xs))
|
|
.child(WordCount::new(text.clone()))
|
|
.child(ReadingTime::new(text)),
|
|
)
|
|
.when(*linking.read(cx), |editor| {
|
|
editor.child(LinkEditor::new(
|
|
(self.id.clone(), "link"),
|
|
&self.field,
|
|
move |window, cx| close_link(window, cx),
|
|
))
|
|
})
|
|
}
|
|
}
|