text_view: Refactor TextView to RenderOnce to easy usage. (#647)
This commit is contained in:
parent
c303b84303
commit
d54dcdf417
5 changed files with 227 additions and 114 deletions
|
|
@ -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())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue