diff --git a/crates/story/examples/html.rs b/crates/story/examples/html.rs
index 6a45ffff..c4296143 100644
--- a/crates/story/examples/html.rs
+++ b/crates/story/examples/html.rs
@@ -4,7 +4,6 @@ use story::Assets;
pub struct Example {
text_input: Entity,
- text_view: Entity,
_subscribe: Subscription,
}
@@ -18,15 +17,11 @@ impl Example {
.rows(50)
.placeholder("Input your HTML here...")
});
- let text_view = cx.new(|cx| TextView::html(EXAMPLE, cx));
let _subscribe = cx.subscribe(
&text_input,
- |this, _, _: &gpui_component::input::InputEvent, cx| {
- let new_text = this.text_input.read(cx).text();
- this.text_view.update(cx, |view, cx| {
- view.set_text(new_text, cx);
- });
+ |_, _, _: &gpui_component::input::InputEvent, cx| {
+ cx.notify();
},
);
@@ -36,7 +31,6 @@ impl Example {
Self {
text_input,
- text_view,
_subscribe,
}
}
@@ -70,7 +64,7 @@ impl Render for Example {
.p_5()
.flex_1()
.overflow_y_scroll()
- .child(self.text_view.clone()),
+ .child(TextView::html("preview", self.text_input.read(cx).text())),
)
}
}
diff --git a/crates/story/examples/markdown.rs b/crates/story/examples/markdown.rs
index e0838084..103a82d7 100644
--- a/crates/story/examples/markdown.rs
+++ b/crates/story/examples/markdown.rs
@@ -2,17 +2,13 @@ use gpui::*;
use gpui_component::{text::TextView, ActiveTheme as _};
use story::Assets;
-pub struct Example {
- text_view: Entity,
-}
+pub struct Example {}
const EXAMPLE: &str = include_str!("./markdown.md");
impl Example {
- pub fn new(_: &mut Window, cx: &mut Context) -> Self {
- let text_view = cx.new(|cx| TextView::markdown(EXAMPLE, cx));
-
- Self { text_view }
+ pub fn new(_: &mut Window, _: &mut Context) -> Self {
+ Self {}
}
fn view(window: &mut Window, cx: &mut App) -> Entity {
@@ -46,7 +42,7 @@ impl Render for Example {
.p_5()
.flex_1()
.overflow_y_scroll()
- .child(self.text_view.clone()),
+ .child(TextView::markdown("preview", EXAMPLE)),
)
}
}
diff --git a/crates/ui/src/text/html.rs b/crates/ui/src/text/html.rs
index 9278b915..0105f6fa 100644
--- a/crates/ui/src/text/html.rs
+++ b/crates/ui/src/text/html.rs
@@ -5,14 +5,17 @@ use std::collections::HashMap;
use std::ops::Range;
use std::rc::Rc;
+use gpui::prelude::FluentBuilder as _;
use gpui::{
- div, px, relative, Context, DefiniteLength, IntoElement, ParentElement as _, Render,
- SharedString,
+ div, px, relative, AnyElement, DefiniteLength, Element, ElementId, IntoElement,
+ ParentElement as _, SharedString, Styled as _, Window,
};
use html5ever::tendril::TendrilSink;
use html5ever::{local_name, parse_document, LocalName, ParseOpts};
use markup5ever_rcdom::{Node, NodeData, RcDom};
+use crate::v_flex;
+
use super::element::{
self, ImageNode, InlineTextStyle, LinkMark, Paragraph, Table, TableRow, TextNode,
};
@@ -53,7 +56,7 @@ const BLOCK_ELEMENTS: [&str; 33] = [
"ul",
];
-pub(super) fn parse_html(source: &str) -> Result {
+pub(super) fn parse_html(source: &str) -> Result {
let opts = ParseOpts {
..Default::default()
};
@@ -64,7 +67,8 @@ pub(super) fn parse_html(source: &str) -> Result
// https://github.com/servo/html5ever/blob/main/rcdom/examples/print-rcdom.rs
let dom = parse_document(RcDom::default(), opts)
.from_utf8()
- .read_from(&mut cursor)?;
+ .read_from(&mut cursor)
+ .map_err(|e| SharedString::from(format!("{:?}", e)))?;
let mut paragraph = Paragraph::default();
// NOTE: The outer paragraph is not used.
@@ -74,45 +78,115 @@ pub(super) fn parse_html(source: &str) -> Result
Ok(node)
}
-pub struct HtmlView {
+pub(super) struct HtmlElement {
+ id: ElementId,
text: SharedString,
- parsed: bool,
- node: Option,
}
-impl HtmlView {
- pub fn new(raw: impl Into) -> Self {
+impl HtmlElement {
+ pub(super) fn new(id: impl Into, raw: impl Into) -> Self {
Self {
+ id: id.into(),
text: raw.into(),
- parsed: false,
- node: None,
}
}
- pub fn set_text(&mut self, raw: impl Into, cx: &mut Context) {
+ /// Set the source of the markdown view.
+ pub(crate) fn text(mut self, raw: impl Into) -> Self {
self.text = raw.into();
- self.parsed = false;
- self.node = None;
- cx.notify();
- }
-
- fn parse_if_needed(&mut self) {
- if !self.parsed {
- self.node = parse_html(&self.text).ok();
- self.parsed = true;
- }
+ self
}
}
-impl Render for HtmlView {
- fn render(&mut self, _: &mut gpui::Window, _: &mut Context<'_, Self>) -> impl IntoElement {
- self.parse_if_needed();
+#[derive(Default)]
+pub struct HtmlState {
+ raw: SharedString,
+ root: Option>,
+}
- if let Some(node) = &self.node {
- div().child(node.clone())
- } else {
- div()
+impl HtmlState {
+ fn parse_if_needed(&mut self, new_text: SharedString) {
+ let is_changed = self.raw != new_text;
+
+ if self.root.is_some() && !is_changed {
+ return;
}
+
+ self.raw = new_text;
+ self.root = Some(parse_html(&self.raw));
+ }
+}
+
+impl IntoElement for HtmlElement {
+ type Element = Self;
+
+ fn into_element(self) -> Self::Element {
+ self
+ }
+}
+
+impl Element for HtmlElement {
+ type RequestLayoutState = AnyElement;
+ type PrepaintState = ();
+
+ fn id(&self) -> Option {
+ Some(self.id.clone())
+ }
+
+ fn request_layout(
+ &mut self,
+ id: Option<&gpui::GlobalElementId>,
+ window: &mut Window,
+ cx: &mut gpui::App,
+ ) -> (gpui::LayoutId, Self::RequestLayoutState) {
+ window.with_element_state(id.unwrap(), |state, window| {
+ let mut state: HtmlState = state.unwrap_or_default();
+ state.parse_if_needed(self.text.clone());
+
+ let root = state
+ .root
+ .clone()
+ .expect("BUG: root should not None, maybe parse_if_needed issue.");
+
+ let mut el = div()
+ .map(|this| match root {
+ Ok(node) => this.child(node),
+ Err(err) => this.child(
+ v_flex()
+ .gap_1()
+ .child("Error parsing HTML")
+ .child(err.to_string()),
+ ),
+ })
+ .into_any_element();
+
+ let layout_id = el.request_layout(window, cx);
+
+ ((layout_id, el), state)
+ })
+ }
+
+ fn prepaint(
+ &mut self,
+ _: Option<&gpui::GlobalElementId>,
+ _: gpui::Bounds,
+ request_layout: &mut Self::RequestLayoutState,
+ window: &mut Window,
+ cx: &mut gpui::App,
+ ) -> Self::PrepaintState {
+ request_layout.prepaint(window, cx);
+ }
+
+ fn paint(
+ &mut self,
+ _: Option<&gpui::GlobalElementId>,
+ _: gpui::Bounds,
+ request_layout: &mut Self::RequestLayoutState,
+ _: &mut Self::PrepaintState,
+ window: &mut Window,
+ cx: &mut gpui::App,
+ ) {
+ request_layout.paint(window, cx);
}
}
diff --git a/crates/ui/src/text/markdown.rs b/crates/ui/src/text/markdown.rs
index 6a83044b..6dbc56f4 100644
--- a/crates/ui/src/text/markdown.rs
+++ b/crates/ui/src/text/markdown.rs
@@ -1,6 +1,6 @@
use gpui::{
- div, prelude::FluentBuilder as _, Context, IntoElement, ParentElement, Render, SharedString,
- Styled, Window,
+ div, prelude::FluentBuilder as _, AnyElement, Element, ElementId, IntoElement, ParentElement,
+ SharedString, Styled, Window,
};
use markdown::{
mdast::{self, Node},
@@ -30,56 +30,120 @@ use super::{
/// - As a markdown editor.
/// - Add custom markdown syntax.
/// - Complex styles cumstomization.
-pub(super) struct MarkdownView {
+pub(super) struct MarkdownElement {
+ id: ElementId,
text: SharedString,
- parsed: bool,
- root: Option>,
}
-impl MarkdownView {
- pub(super) fn new(raw: impl Into) -> Self {
+impl MarkdownElement {
+ pub(super) fn new(id: impl Into, raw: impl Into) -> Self {
Self {
+ id: id.into(),
text: raw.into(),
- parsed: false,
- root: None,
}
}
/// Set the source of the markdown view.
- pub(crate) fn set_text(&mut self, raw: impl Into, cx: &mut Context) {
+ pub(crate) fn text(mut self, raw: impl Into) -> Self {
self.text = raw.into();
- self.parsed = false;
- cx.notify();
- }
-
- fn parse_if_needed(&mut self) {
- if self.parsed {
- return;
- }
-
- self.root = Some(markdown::to_mdast(&self.text, &ParseOptions::gfm()).map(|n| n.into()));
- self.parsed = true;
+ self
}
}
-impl Render for MarkdownView {
- fn render(&mut self, _: &mut Window, _: &mut gpui::Context<'_, Self>) -> impl IntoElement {
- self.parse_if_needed();
+#[derive(Default)]
+pub struct MarkdownState {
+ raw: SharedString,
+ root: Option>,
+}
- let Some(root) = self.root.clone() else {
- return div();
- };
+impl MarkdownState {
+ fn parse_if_needed(&mut self, new_text: SharedString) {
+ let is_changed = self.raw != new_text;
- div().map(|this| match root {
- Ok(node) => this.child(node),
- Err(err) => this.child(
- v_flex()
- .gap_1()
- .child("Error parsing markdown")
- .child(err.to_string()),
- ),
+ if self.root.is_some() && !is_changed {
+ return;
+ }
+
+ self.raw = new_text;
+ self.root = Some(
+ markdown::to_mdast(&self.raw, &ParseOptions::gfm())
+ .map(|n| n.into())
+ .map_err(|e| e.to_string().into()),
+ );
+ }
+}
+
+impl IntoElement for MarkdownElement {
+ type Element = Self;
+
+ fn into_element(self) -> Self::Element {
+ self
+ }
+}
+
+impl Element for MarkdownElement {
+ type RequestLayoutState = AnyElement;
+ type PrepaintState = ();
+
+ fn id(&self) -> Option {
+ Some(self.id.clone())
+ }
+
+ fn request_layout(
+ &mut self,
+ id: Option<&gpui::GlobalElementId>,
+ window: &mut Window,
+ cx: &mut gpui::App,
+ ) -> (gpui::LayoutId, Self::RequestLayoutState) {
+ window.with_element_state(id.unwrap(), |state, window| {
+ let mut state: MarkdownState = state.unwrap_or_default();
+ state.parse_if_needed(self.text.clone());
+
+ let root = state
+ .root
+ .clone()
+ .expect("BUG: root should not None, maybe parse_if_needed issue.");
+
+ let mut el = div()
+ .map(|this| match root {
+ Ok(node) => this.child(node),
+ Err(err) => this.child(
+ v_flex()
+ .gap_1()
+ .child("Error parsing Markdown")
+ .child(err.to_string()),
+ ),
+ })
+ .into_any_element();
+
+ let layout_id = el.request_layout(window, cx);
+
+ ((layout_id, el), state)
})
}
+
+ fn prepaint(
+ &mut self,
+ _: Option<&gpui::GlobalElementId>,
+ _: gpui::Bounds,
+ request_layout: &mut Self::RequestLayoutState,
+ window: &mut Window,
+ cx: &mut gpui::App,
+ ) -> Self::PrepaintState {
+ request_layout.prepaint(window, cx);
+ }
+
+ fn paint(
+ &mut self,
+ _: Option<&gpui::GlobalElementId>,
+ _: gpui::Bounds,
+ request_layout: &mut Self::RequestLayoutState,
+ _: &mut Self::PrepaintState,
+ window: &mut Window,
+ cx: &mut gpui::App,
+ ) {
+ request_layout.paint(window, cx);
+ }
}
fn parse_table_row(table: &mut Table, node: &mdast::TableRow) {
@@ -380,16 +444,3 @@ impl From for element::Node {
}
}
}
-
-#[cfg(test)]
-mod tests {
- use super::MarkdownView;
-
- #[test]
- fn test_parse() {
- let source = include_str!("../../../story/examples/markdown.md");
- let mut renderer = MarkdownView::new(source);
- renderer.parse_if_needed();
- // println!("{:#?}", renderer.root);
- }
-}
diff --git a/crates/ui/src/text/mod.rs b/crates/ui/src/text/mod.rs
index 18b7396f..a2b21457 100644
--- a/crates/ui/src/text/mod.rs
+++ b/crates/ui/src/text/mod.rs
@@ -1,47 +1,45 @@
-use gpui::{App, AppContext, Entity, IntoElement, Render, SharedString};
-use html::HtmlView;
-use markdown::MarkdownView;
+use gpui::{App, ElementId, IntoElement, RenderOnce, SharedString, Window};
+use html::HtmlElement;
+use markdown::MarkdownElement;
mod element;
mod html;
mod markdown;
mod utils;
+/// A text view that can render Markdown or HTML.
#[allow(private_interfaces)]
+#[derive(IntoElement)]
pub enum TextView {
- Markdown(Entity),
- Html(Entity),
+ Markdown(MarkdownElement),
+ Html(HtmlElement),
}
impl TextView {
/// Create a new markdown text view.
- pub fn markdown(raw: impl Into, cx: &mut App) -> Self {
- Self::Markdown(cx.new(|_| MarkdownView::new(raw)))
+ pub fn markdown(id: impl Into, raw: impl Into) -> Self {
+ Self::Markdown(MarkdownElement::new(id, raw))
}
/// Create a new html text view.
- pub fn html(raw: impl Into, cx: &mut App) -> Self {
- Self::Html(cx.new(|_| HtmlView::new(raw)))
+ pub fn html(id: impl Into, raw: impl Into) -> Self {
+ Self::Html(HtmlElement::new(id, raw))
}
/// Set the source text of the text view.
- pub fn set_text(&mut self, raw: impl Into, cx: &mut App) {
+ pub fn text(self, raw: impl Into) -> Self {
match self {
- Self::Markdown(view) => view.update(cx, |this, cx| this.set_text(raw, cx)),
- Self::Html(view) => view.update(cx, |this, cx| this.set_text(raw, cx)),
+ Self::Markdown(el) => Self::Markdown(el.text(raw)),
+ Self::Html(el) => Self::Html(el.text(raw)),
}
}
}
-impl Render for TextView {
- fn render(
- &mut self,
- _: &mut gpui::Window,
- _: &mut gpui::Context<'_, Self>,
- ) -> impl IntoElement {
+impl RenderOnce for TextView {
+ fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
match self {
- Self::Markdown(view) => view.clone().into_any_element(),
- Self::Html(view) => view.clone().into_any_element(),
+ Self::Markdown(el) => el.into_any_element(),
+ Self::Html(el) => el.into_any_element(),
}
}
}