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(), } } }