text_view: Refactor TextView to RenderOnce to easy usage. (#647)

This commit is contained in:
Jason Lee 2025-02-24 14:13:50 +08:00 committed by GitHub
parent c303b84303
commit d54dcdf417
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 227 additions and 114 deletions

View file

@ -4,7 +4,6 @@ use story::Assets;
pub struct Example { pub struct Example {
text_input: Entity<TextInput>, text_input: Entity<TextInput>,
text_view: Entity<TextView>,
_subscribe: Subscription, _subscribe: Subscription,
} }
@ -18,15 +17,11 @@ impl Example {
.rows(50) .rows(50)
.placeholder("Input your HTML here...") .placeholder("Input your HTML here...")
}); });
let text_view = cx.new(|cx| TextView::html(EXAMPLE, cx));
let _subscribe = cx.subscribe( let _subscribe = cx.subscribe(
&text_input, &text_input,
|this, _, _: &gpui_component::input::InputEvent, cx| { |_, _, _: &gpui_component::input::InputEvent, cx| {
let new_text = this.text_input.read(cx).text(); cx.notify();
this.text_view.update(cx, |view, cx| {
view.set_text(new_text, cx);
});
}, },
); );
@ -36,7 +31,6 @@ impl Example {
Self { Self {
text_input, text_input,
text_view,
_subscribe, _subscribe,
} }
} }
@ -70,7 +64,7 @@ impl Render for Example {
.p_5() .p_5()
.flex_1() .flex_1()
.overflow_y_scroll() .overflow_y_scroll()
.child(self.text_view.clone()), .child(TextView::html("preview", self.text_input.read(cx).text())),
) )
} }
} }

View file

@ -2,17 +2,13 @@ use gpui::*;
use gpui_component::{text::TextView, ActiveTheme as _}; use gpui_component::{text::TextView, ActiveTheme as _};
use story::Assets; use story::Assets;
pub struct Example { pub struct Example {}
text_view: Entity<TextView>,
}
const EXAMPLE: &str = include_str!("./markdown.md"); const EXAMPLE: &str = include_str!("./markdown.md");
impl Example { impl Example {
pub fn new(_: &mut Window, cx: &mut Context<Self>) -> Self { pub fn new(_: &mut Window, _: &mut Context<Self>) -> Self {
let text_view = cx.new(|cx| TextView::markdown(EXAMPLE, cx)); Self {}
Self { text_view }
} }
fn view(window: &mut Window, cx: &mut App) -> Entity<Self> { fn view(window: &mut Window, cx: &mut App) -> Entity<Self> {
@ -46,7 +42,7 @@ impl Render for Example {
.p_5() .p_5()
.flex_1() .flex_1()
.overflow_y_scroll() .overflow_y_scroll()
.child(self.text_view.clone()), .child(TextView::markdown("preview", EXAMPLE)),
) )
} }
} }

View file

@ -5,14 +5,17 @@ use std::collections::HashMap;
use std::ops::Range; use std::ops::Range;
use std::rc::Rc; use std::rc::Rc;
use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
div, px, relative, Context, DefiniteLength, IntoElement, ParentElement as _, Render, div, px, relative, AnyElement, DefiniteLength, Element, ElementId, IntoElement,
SharedString, ParentElement as _, SharedString, Styled as _, Window,
}; };
use html5ever::tendril::TendrilSink; use html5ever::tendril::TendrilSink;
use html5ever::{local_name, parse_document, LocalName, ParseOpts}; use html5ever::{local_name, parse_document, LocalName, ParseOpts};
use markup5ever_rcdom::{Node, NodeData, RcDom}; use markup5ever_rcdom::{Node, NodeData, RcDom};
use crate::v_flex;
use super::element::{ use super::element::{
self, ImageNode, InlineTextStyle, LinkMark, Paragraph, Table, TableRow, TextNode, self, ImageNode, InlineTextStyle, LinkMark, Paragraph, Table, TableRow, TextNode,
}; };
@ -53,7 +56,7 @@ const BLOCK_ELEMENTS: [&str; 33] = [
"ul", "ul",
]; ];
pub(super) fn parse_html(source: &str) -> Result<element::Node, std::io::Error> { pub(super) fn parse_html(source: &str) -> Result<element::Node, SharedString> {
let opts = ParseOpts { let opts = ParseOpts {
..Default::default() ..Default::default()
}; };
@ -64,7 +67,8 @@ pub(super) fn parse_html(source: &str) -> Result<element::Node, std::io::Error>
// https://github.com/servo/html5ever/blob/main/rcdom/examples/print-rcdom.rs // https://github.com/servo/html5ever/blob/main/rcdom/examples/print-rcdom.rs
let dom = parse_document(RcDom::default(), opts) let dom = parse_document(RcDom::default(), opts)
.from_utf8() .from_utf8()
.read_from(&mut cursor)?; .read_from(&mut cursor)
.map_err(|e| SharedString::from(format!("{:?}", e)))?;
let mut paragraph = Paragraph::default(); let mut paragraph = Paragraph::default();
// NOTE: The outer paragraph is not used. // NOTE: The outer paragraph is not used.
@ -74,45 +78,115 @@ pub(super) fn parse_html(source: &str) -> Result<element::Node, std::io::Error>
Ok(node) Ok(node)
} }
pub struct HtmlView { pub(super) struct HtmlElement {
id: ElementId,
text: SharedString, text: SharedString,
parsed: bool,
node: Option<element::Node>,
} }
impl HtmlView { impl HtmlElement {
pub fn new(raw: impl Into<SharedString>) -> Self { pub(super) fn new(id: impl Into<ElementId>, raw: impl Into<SharedString>) -> Self {
Self { Self {
id: id.into(),
text: raw.into(), text: raw.into(),
parsed: false,
node: None,
} }
} }
pub fn set_text(&mut self, raw: impl Into<SharedString>, cx: &mut Context<Self>) { /// Set the source of the markdown view.
pub(crate) fn text(mut self, raw: impl Into<SharedString>) -> Self {
self.text = raw.into(); self.text = raw.into();
self.parsed = false; self
self.node = None;
cx.notify();
}
fn parse_if_needed(&mut self) {
if !self.parsed {
self.node = parse_html(&self.text).ok();
self.parsed = true;
}
} }
} }
impl Render for HtmlView { #[derive(Default)]
fn render(&mut self, _: &mut gpui::Window, _: &mut Context<'_, Self>) -> impl IntoElement { pub struct HtmlState {
self.parse_if_needed(); raw: SharedString,
root: Option<Result<element::Node, SharedString>>,
}
if let Some(node) = &self.node { impl HtmlState {
div().child(node.clone()) fn parse_if_needed(&mut self, new_text: SharedString) {
} else { let is_changed = self.raw != new_text;
div()
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<gpui::ElementId> {
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<gpui::Pixels>,
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<gpui::Pixels>,
request_layout: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut gpui::App,
) {
request_layout.paint(window, cx);
} }
} }

View file

@ -1,6 +1,6 @@
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, Context, IntoElement, ParentElement, Render, SharedString, div, prelude::FluentBuilder as _, AnyElement, Element, ElementId, IntoElement, ParentElement,
Styled, Window, SharedString, Styled, Window,
}; };
use markdown::{ use markdown::{
mdast::{self, Node}, mdast::{self, Node},
@ -30,56 +30,120 @@ use super::{
/// - As a markdown editor. /// - As a markdown editor.
/// - Add custom markdown syntax. /// - Add custom markdown syntax.
/// - Complex styles cumstomization. /// - Complex styles cumstomization.
pub(super) struct MarkdownView { pub(super) struct MarkdownElement {
id: ElementId,
text: SharedString, text: SharedString,
parsed: bool,
root: Option<Result<element::Node, markdown::message::Message>>,
} }
impl MarkdownView { impl MarkdownElement {
pub(super) fn new(raw: impl Into<SharedString>) -> Self { pub(super) fn new(id: impl Into<ElementId>, raw: impl Into<SharedString>) -> Self {
Self { Self {
id: id.into(),
text: raw.into(), text: raw.into(),
parsed: false,
root: None,
} }
} }
/// Set the source of the markdown view. /// Set the source of the markdown view.
pub(crate) fn set_text(&mut self, raw: impl Into<SharedString>, cx: &mut Context<Self>) { pub(crate) fn text(mut self, raw: impl Into<SharedString>) -> Self {
self.text = raw.into(); self.text = raw.into();
self.parsed = false; self
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;
} }
} }
impl Render for MarkdownView { #[derive(Default)]
fn render(&mut self, _: &mut Window, _: &mut gpui::Context<'_, Self>) -> impl IntoElement { pub struct MarkdownState {
self.parse_if_needed(); raw: SharedString,
root: Option<Result<element::Node, SharedString>>,
}
let Some(root) = self.root.clone() else { impl MarkdownState {
return div(); fn parse_if_needed(&mut self, new_text: SharedString) {
}; let is_changed = self.raw != new_text;
div().map(|this| match root { if self.root.is_some() && !is_changed {
Ok(node) => this.child(node), return;
Err(err) => this.child( }
v_flex()
.gap_1() self.raw = new_text;
.child("Error parsing markdown") self.root = Some(
.child(err.to_string()), 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<gpui::ElementId> {
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<gpui::Pixels>,
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<gpui::Pixels>,
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) { fn parse_table_row(table: &mut Table, node: &mdast::TableRow) {
@ -380,16 +444,3 @@ impl From<mdast::Node> 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);
}
}

View file

@ -1,47 +1,45 @@
use gpui::{App, AppContext, Entity, IntoElement, Render, SharedString}; use gpui::{App, ElementId, IntoElement, RenderOnce, SharedString, Window};
use html::HtmlView; use html::HtmlElement;
use markdown::MarkdownView; use markdown::MarkdownElement;
mod element; mod element;
mod html; mod html;
mod markdown; mod markdown;
mod utils; mod utils;
/// A text view that can render Markdown or HTML.
#[allow(private_interfaces)] #[allow(private_interfaces)]
#[derive(IntoElement)]
pub enum TextView { pub enum TextView {
Markdown(Entity<MarkdownView>), Markdown(MarkdownElement),
Html(Entity<HtmlView>), Html(HtmlElement),
} }
impl TextView { impl TextView {
/// Create a new markdown text view. /// Create a new markdown text view.
pub fn markdown(raw: impl Into<SharedString>, cx: &mut App) -> Self { pub fn markdown(id: impl Into<ElementId>, raw: impl Into<SharedString>) -> Self {
Self::Markdown(cx.new(|_| MarkdownView::new(raw))) Self::Markdown(MarkdownElement::new(id, raw))
} }
/// Create a new html text view. /// Create a new html text view.
pub fn html(raw: impl Into<SharedString>, cx: &mut App) -> Self { pub fn html(id: impl Into<ElementId>, raw: impl Into<SharedString>) -> Self {
Self::Html(cx.new(|_| HtmlView::new(raw))) Self::Html(HtmlElement::new(id, raw))
} }
/// Set the source text of the text view. /// Set the source text of the text view.
pub fn set_text(&mut self, raw: impl Into<SharedString>, cx: &mut App) { pub fn text(self, raw: impl Into<SharedString>) -> Self {
match self { match self {
Self::Markdown(view) => view.update(cx, |this, cx| this.set_text(raw, cx)), Self::Markdown(el) => Self::Markdown(el.text(raw)),
Self::Html(view) => view.update(cx, |this, cx| this.set_text(raw, cx)), Self::Html(el) => Self::Html(el.text(raw)),
} }
} }
} }
impl Render for TextView { impl RenderOnce for TextView {
fn render( fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
&mut self,
_: &mut gpui::Window,
_: &mut gpui::Context<'_, Self>,
) -> impl IntoElement {
match self { match self {
Self::Markdown(view) => view.clone().into_any_element(), Self::Markdown(el) => el.into_any_element(),
Self::Html(view) => view.clone().into_any_element(), Self::Html(el) => el.into_any_element(),
} }
} }
} }