text_view: Reworks TextView to defer state initialization until first layout. (#1310)

This commit is contained in:
Sunli 2025-09-30 13:50:27 +08:00 committed by GitHub
parent d5ae887cbb
commit 880c019fb8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -44,7 +44,7 @@ impl RenderOnce for TextViewElement {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
self.state.update(cx, |state, cx| { self.state.update(cx, |state, cx| {
div().map(|this| match &mut state.parsed_result { div().map(|this| match &mut state.parsed_result {
Ok(content) => this.child(content.root_node.render( Some(Ok(content)) => this.child(content.root_node.render(
None, None,
true, true,
true, true,
@ -52,12 +52,13 @@ impl RenderOnce for TextViewElement {
window, window,
cx, cx,
)), )),
Err(err) => this.child( Some(Err(err)) => this.child(
v_flex() v_flex()
.gap_1() .gap_1()
.child("Failed to parse content") .child("Failed to parse content")
.child(err.to_string()), .child(err.to_string()),
), ),
None => this,
}) })
}) })
} }
@ -82,9 +83,9 @@ impl RenderOnce for TextViewElement {
#[derive(Clone)] #[derive(Clone)]
pub struct TextView { pub struct TextView {
id: ElementId, id: ElementId,
init_state: Option<InitState>,
state: Entity<TextViewState>, state: Entity<TextViewState>,
selectable: bool, selectable: bool,
tx: smol::channel::Sender<Update>,
} }
#[derive(PartialEq)] #[derive(PartialEq)]
@ -121,6 +122,8 @@ struct UpdateFuture {
impl UpdateFuture { impl UpdateFuture {
fn new( fn new(
type_: TextViewType, type_: TextViewType,
style: TextViewStyle,
text: SharedString,
highlight_theme: Arc<HighlightTheme>, highlight_theme: Arc<HighlightTheme>,
rx: smol::channel::Receiver<Update>, rx: smol::channel::Receiver<Update>,
tx_result: smol::channel::Sender<Result<ParsedContent, SharedString>>, tx_result: smol::channel::Sender<Result<ParsedContent, SharedString>>,
@ -129,8 +132,8 @@ impl UpdateFuture {
Self { Self {
type_, type_,
highlight_theme, highlight_theme,
current_style: TextViewStyle::default(), current_style: style,
current_text: SharedString::default(), current_text: text,
timer: Timer::never(), timer: Timer::never(),
rx, rx,
tx_result, tx_result,
@ -184,11 +187,23 @@ impl Future for UpdateFuture {
} }
} }
#[derive(Clone)]
enum InitState {
Initializing {
type_: TextViewType,
text: SharedString,
style: Box<TextViewStyle>,
highlight_theme: Arc<HighlightTheme>,
},
Initialized {
tx: smol::channel::Sender<Update>,
},
}
pub(crate) struct TextViewState { pub(crate) struct TextViewState {
parent_entity: Option<EntityId>, parent_entity: Option<EntityId>,
tx: smol::channel::Sender<Update>, tx: Option<smol::channel::Sender<Update>>,
parsed_result: Result<ParsedContent, SharedString>, parsed_result: Option<Result<ParsedContent, SharedString>>,
focus_handle: Option<FocusHandle>, focus_handle: Option<FocusHandle>,
/// The bounds of the text view /// The bounds of the text view
@ -201,47 +216,13 @@ pub(crate) struct TextViewState {
} }
impl TextViewState { impl TextViewState {
fn new( fn new(cx: &mut Context<TextViewState>) -> Self {
type_: TextViewType,
content: &str,
window: &mut Window,
cx: &mut Context<TextViewState>,
) -> Self {
let focus_handle = cx.focus_handle(); let focus_handle = cx.focus_handle();
let (tx, rx) = smol::channel::unbounded::<Update>();
let (tx_result, rx_result) =
smol::channel::unbounded::<Result<ParsedContent, SharedString>>();
let highlight_theme = cx.theme().highlight_theme.clone();
let parsed_result = parse_content(type_, content, Default::default(), &highlight_theme);
cx.spawn_in(window, async move |this, cx| {
while let Ok(parsed_result) = rx_result.recv().await {
_ = this.update(cx, |state, cx| {
state.parsed_result = parsed_result;
if let Some(parent_entity) = state.parent_entity {
let app = &mut **cx;
app.notify(parent_entity);
}
state.clear_selection();
});
}
})
.detach();
cx.background_spawn(UpdateFuture::new(
type_,
highlight_theme,
rx,
tx_result,
Duration::from_millis(200),
))
.detach();
Self { Self {
parent_entity: None, parent_entity: None,
tx, tx: None,
parsed_result: None,
focus_handle: Some(focus_handle), focus_handle: Some(focus_handle),
parsed_result,
bounds: Bounds::default(), bounds: Bounds::default(),
selection_positions: (None, None), selection_positions: (None, None),
is_selecting: false, is_selecting: false,
@ -303,14 +284,21 @@ impl TextViewState {
} }
fn selection_text(&self) -> Option<String> { fn selection_text(&self) -> Option<String> {
Some(self.parsed_result.as_ref().ok()?.root_node.selected_text()) Some(
self.parsed_result
.as_ref()?
.as_ref()
.ok()?
.root_node
.selected_text(),
)
} }
} }
#[derive(IntoElement, Clone)] #[derive(IntoElement, Clone)]
pub enum Text { pub enum Text {
String(SharedString), String(SharedString),
TextView(TextView), TextView(Box<TextView>),
} }
impl From<SharedString> for Text { impl From<SharedString> for Text {
@ -333,7 +321,7 @@ impl From<String> for Text {
impl From<TextView> for Text { impl From<TextView> for Text {
fn from(e: TextView) -> Self { fn from(e: TextView) -> Self {
Self::TextView(e) Self::TextView(Box::new(e))
} }
} }
@ -344,7 +332,7 @@ impl Text {
pub fn style(self, style: TextViewStyle) -> Self { pub fn style(self, style: TextViewStyle) -> Self {
match self { match self {
Self::String(s) => Self::String(s), Self::String(s) => Self::String(s),
Self::TextView(e) => Self::TextView(e.style(style)), Self::TextView(e) => Self::TextView(Box::new(e.style(style))),
} }
} }
} }
@ -359,6 +347,26 @@ impl RenderOnce for Text {
} }
impl TextView { impl TextView {
fn create_init_state(
type_: TextViewType,
text: &SharedString,
highlight_theme: &Arc<HighlightTheme>,
state: &Entity<TextViewState>,
cx: &mut App,
) -> InitState {
let state = state.read(cx);
if let Some(tx) = &state.tx {
InitState::Initialized { tx: tx.clone() }
} else {
InitState::Initializing {
type_,
text: text.clone(),
style: Default::default(),
highlight_theme: highlight_theme.clone(),
}
}
}
/// Create a new markdown text view. /// Create a new markdown text view.
pub fn markdown( pub fn markdown(
id: impl Into<ElementId>, id: impl Into<ElementId>,
@ -368,26 +376,26 @@ impl TextView {
) -> Self { ) -> Self {
let id: ElementId = id.into(); let id: ElementId = id.into();
let markdown = markdown.into(); let markdown = markdown.into();
let mut is_new_state = false; let highlight_theme = cx.theme().highlight_theme.clone();
let state = window.use_keyed_state( let state =
SharedString::from(format!("{}/state", id)), window.use_keyed_state(SharedString::from(format!("{}/state", id)), cx, |_, cx| {
cx, TextViewState::new(cx)
|window, cx| {
is_new_state = true;
TextViewState::new(TextViewType::Markdown, &markdown, window, cx)
},
);
let tx = state.read(cx).tx.clone();
if !is_new_state {
state.update(cx, move |state, _| {
_ = state.tx.try_send(Update::Text(markdown));
}); });
let init_state = Self::create_init_state(
TextViewType::Markdown,
&markdown,
&highlight_theme,
&state,
cx,
);
if let Some(tx) = &state.read(cx).tx {
let _ = tx.try_send(Update::Text(markdown));
} }
Self { Self {
id, id,
init_state: Some(init_state),
state, state,
selectable: false, selectable: false,
tx,
} }
} }
@ -400,38 +408,48 @@ impl TextView {
) -> Self { ) -> Self {
let id: ElementId = id.into(); let id: ElementId = id.into();
let html = html.into(); let html = html.into();
let mut is_new_state = false; let highlight_theme = cx.theme().highlight_theme.clone();
let state = window.use_keyed_state( let state =
SharedString::from(format!("{}/state", id)), window.use_keyed_state(SharedString::from(format!("{}/state", id)), cx, |_, cx| {
cx, TextViewState::new(cx)
|window, cx| {
is_new_state = true;
TextViewState::new(TextViewType::Html, &html, window, cx)
},
);
let tx = state.read(cx).tx.clone();
if !is_new_state {
state.update(cx, move |state, _| {
_ = state.tx.try_send(Update::Text(html));
}); });
let init_state =
Self::create_init_state(TextViewType::Html, &html, &highlight_theme, &state, cx);
if let Some(tx) = &state.read(cx).tx {
let _ = tx.try_send(Update::Text(html));
} }
Self { Self {
id, id,
init_state: Some(init_state),
state, state,
selectable: false, selectable: false,
tx,
} }
} }
/// Set the source text of the text view. /// Set the source text of the text view.
pub fn text(self, raw: impl Into<SharedString>) -> Self { pub fn text(mut self, raw: impl Into<SharedString>) -> Self {
_ = self.tx.try_send(Update::Text(raw.into())); if let Some(init_state) = &mut self.init_state {
match init_state {
InitState::Initializing { text, .. } => *text = raw.into(),
InitState::Initialized { tx } => {
let _ = tx.try_send(Update::Text(raw.into()));
}
}
}
self self
} }
/// Set [`TextViewStyle`]. /// Set [`TextViewStyle`].
pub fn style(self, style: TextViewStyle) -> Self { pub fn style(mut self, style: TextViewStyle) -> Self {
_ = self.tx.try_send(Update::Style(Box::new(style))); if let Some(init_state) = &mut self.init_state {
match init_state {
InitState::Initializing { style: s, .. } => *s = Box::new(style),
InitState::Initialized { tx } => {
let _ = tx.try_send(Update::Style(Box::new(style)));
}
}
}
self self
} }
@ -477,6 +495,59 @@ impl Element for TextView {
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) { ) -> (LayoutId, Self::RequestLayoutState) {
if let Some(InitState::Initializing {
type_,
text,
style,
highlight_theme,
}) = self.init_state.take()
{
let style = *style;
let highlight_theme = highlight_theme.clone();
let (tx, rx) = smol::channel::unbounded::<Update>();
let (tx_result, rx_result) =
smol::channel::unbounded::<Result<ParsedContent, SharedString>>();
let parsed_result = parse_content(type_, &text, style.clone(), &highlight_theme);
self.state.update(cx, {
let tx = tx.clone();
|state, _| {
state.parsed_result = Some(parsed_result);
state.tx = Some(tx);
}
});
cx.spawn({
let state = self.state.clone();
async move |cx| {
while let Ok(parsed_result) = rx_result.recv().await {
_ = state.update(cx, |state, cx| {
state.parsed_result = Some(parsed_result);
if let Some(parent_entity) = state.parent_entity {
let app = &mut **cx;
app.notify(parent_entity);
}
state.clear_selection();
});
}
}
})
.detach();
cx.background_spawn(UpdateFuture::new(
type_,
style,
text,
highlight_theme,
rx,
tx_result,
Duration::from_millis(200),
))
.detach();
self.init_state = Some(InitState::Initialized { tx });
}
let focus_handle = self let focus_handle = self
.state .state
.read(cx) .read(cx)