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 {
text_input: Entity<TextInput>,
text_view: Entity<TextView>,
_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())),
)
}
}

View file

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

View file

@ -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<element::Node, std::io::Error> {
pub(super) fn parse_html(source: &str) -> Result<element::Node, SharedString> {
let opts = ParseOpts {
..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
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<element::Node, std::io::Error>
Ok(node)
}
pub struct HtmlView {
pub(super) struct HtmlElement {
id: ElementId,
text: SharedString,
parsed: bool,
node: Option<element::Node>,
}
impl HtmlView {
pub fn new(raw: impl Into<SharedString>) -> Self {
impl HtmlElement {
pub(super) fn new(id: impl Into<ElementId>, raw: impl Into<SharedString>) -> Self {
Self {
id: id.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.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<Result<element::Node, SharedString>>,
}
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<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::{
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<Result<element::Node, markdown::message::Message>>,
}
impl MarkdownView {
pub(super) fn new(raw: impl Into<SharedString>) -> Self {
impl MarkdownElement {
pub(super) fn new(id: impl Into<ElementId>, raw: impl Into<SharedString>) -> 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<SharedString>, cx: &mut Context<Self>) {
pub(crate) fn text(mut self, raw: impl Into<SharedString>) -> 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<Result<element::Node, SharedString>>,
}
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<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) {
@ -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 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<MarkdownView>),
Html(Entity<HtmlView>),
Markdown(MarkdownElement),
Html(HtmlElement),
}
impl TextView {
/// Create a new markdown text view.
pub fn markdown(raw: impl Into<SharedString>, cx: &mut App) -> Self {
Self::Markdown(cx.new(|_| MarkdownView::new(raw)))
pub fn markdown(id: impl Into<ElementId>, raw: impl Into<SharedString>) -> Self {
Self::Markdown(MarkdownElement::new(id, raw))
}
/// Create a new html text view.
pub fn html(raw: impl Into<SharedString>, cx: &mut App) -> Self {
Self::Html(cx.new(|_| HtmlView::new(raw)))
pub fn html(id: impl Into<ElementId>, raw: impl Into<SharedString>) -> Self {
Self::Html(HtmlElement::new(id, raw))
}
/// 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 {
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(),
}
}
}