= Vec::with_capacity(children.len());
-
- for (child_ix, child) in children.iter().enumerate() {
- match child {
- Node::Paragraph(_) => {
- let last_not_list = child_ix > 0
- && !matches!(children[child_ix - 1], Node::List { .. });
-
- let text = child.render(
- Some(ListState {
- depth: state.depth + 1,
- ordered: state.ordered,
- todo: checked.is_some(),
- }),
- false,
- true,
- node_cx,
- window,
- cx,
- );
-
- // merge content into last item.
- if last_not_list {
- if let Some(item_item) = items.last_mut() {
- item_item.extend(vec![div()
- .overflow_hidden()
- .child(text)
- .into_any_element()]);
- continue;
- }
- }
-
- items.push(
- h_flex()
- .flex_1()
- .relative()
- .items_start()
- .content_start()
- .when(!state.todo && checked.is_none(), |this| {
- this.child(list_item_prefix(
- ix,
- state.ordered,
- state.depth,
- ))
- })
- .when_some(*checked, |this, checked| {
- // Todo list checkbox
- this.child(
- div()
- .flex()
- .mt(rems(0.4))
- .mr_1p5()
- .size(rems(0.875))
- .items_center()
- .justify_center()
- .rounded(cx.theme().radius.half())
- .border_1()
- .border_color(cx.theme().primary)
- .text_color(cx.theme().primary_foreground)
- .when(checked, |this| {
- this.bg(cx.theme().primary).child(
- Icon::new(IconName::Check)
- .size_2()
- .text_xs(),
- )
- }),
- )
- })
- .child(div().overflow_hidden().child(text)),
- );
- }
- Node::List { .. } => {
- items.push(div().ml(rems(1.)).child(child.render(
- Some(ListState {
- depth: state.depth + 1,
- ordered: state.ordered,
- todo: checked.is_some(),
- }),
- true,
- true,
- node_cx,
- window,
- cx,
- )));
- }
- _ => {}
- }
- }
- items
- })
- .into_any_element(),
- _ => div().into_any_element(),
- }
- }
-
- fn render_table(
- item: &Node,
- node_cx: &NodeContext,
- window: &mut Window,
- cx: &mut App,
- ) -> impl IntoElement {
- const DEFAULT_LENGTH: usize = 5;
- const MAX_LENGTH: usize = 150;
- let col_lens = match item {
- Node::Table(table) => {
- let mut col_lens = vec![];
- for row in table.children.iter() {
- for (ix, cell) in row.children.iter().enumerate() {
- if col_lens.len() <= ix {
- col_lens.push(DEFAULT_LENGTH);
- }
-
- let len = cell.children.text_len();
- if len > col_lens[ix] {
- col_lens[ix] = len;
- }
- }
- }
- col_lens
- }
- _ => vec![],
- };
-
- match item {
- Node::Table(table) => div()
- .id("table")
- .mb(rems(1.))
- .w_full()
- .border_1()
- .border_color(cx.theme().border)
- .rounded(cx.theme().radius)
- .children({
- let mut rows = Vec::with_capacity(table.children.len());
- for (row_ix, row) in table.children.iter().enumerate() {
- rows.push(
- div()
- .id("row")
- .w_full()
- .when(row_ix < table.children.len() - 1, |this| this.border_b_1())
- .border_color(cx.theme().border)
- .flex()
- .flex_row()
- .children({
- let mut cells = Vec::with_capacity(row.children.len());
- for (ix, cell) in row.children.iter().enumerate() {
- let align = table.column_align(ix);
- let is_last_col = ix == row.children.len() - 1;
- let len = col_lens
- .get(ix)
- .copied()
- .unwrap_or(MAX_LENGTH)
- .min(MAX_LENGTH);
-
- cells.push(
- div()
- .id("cell")
- .flex()
- .when(align == ColumnumnAlign::Center, |this| {
- this.justify_center()
- })
- .when(align == ColumnumnAlign::Right, |this| {
- this.justify_end()
- })
- .w(Length::Definite(relative(len as f32)))
- .px_2()
- .py_1()
- .when(!is_last_col, |this| {
- this.border_r_1()
- .border_color(cx.theme().border)
- })
- .truncate()
- .child(cell.children.render(node_cx, window, cx)),
- )
- }
- cells
- }),
- )
- }
- rows
- })
- .into_any_element(),
- _ => div().into_any_element(),
- }
- }
-
- pub(crate) fn render(
- &self,
- list_state: Option
,
- is_root: bool,
- is_last_child: bool,
- node_cx: &NodeContext,
- window: &mut Window,
- cx: &mut App,
- ) -> impl IntoElement {
- let in_list = list_state.is_some();
- let mb = if in_list || is_last_child {
- rems(0.)
- } else {
- node_cx.style.paragraph_gap
- };
-
- match self {
- Node::Root { children } => div()
- .id("div")
- .children({
- let children_len = children.len();
- children.into_iter().enumerate().map(move |(index, c)| {
- let is_last_child = is_root && index == children_len - 1;
- c.render(None, false, is_last_child, node_cx, window, cx)
- })
- })
- .into_any_element(),
- Node::Paragraph(paragraph) => div()
- .id("p")
- .mb(mb)
- .child(paragraph.render(node_cx, window, cx))
- .into_any_element(),
- Node::Heading { level, children } => {
- let (text_size, font_weight) = match level {
- 1 => (rems(2.), FontWeight::BOLD),
- 2 => (rems(1.5), FontWeight::SEMIBOLD),
- 3 => (rems(1.25), FontWeight::SEMIBOLD),
- 4 => (rems(1.125), FontWeight::SEMIBOLD),
- 5 => (rems(1.), FontWeight::SEMIBOLD),
- 6 => (rems(1.), FontWeight::MEDIUM),
- _ => (rems(1.), FontWeight::NORMAL),
- };
-
- let mut text_size = text_size.to_pixels(node_cx.style.heading_base_font_size);
- if let Some(f) = node_cx.style.heading_font_size.as_ref() {
- text_size = (f)(*level, node_cx.style.heading_base_font_size);
- }
-
- h_flex()
- .id(("h", *level as usize))
- .mb(rems(0.3))
- .whitespace_normal()
- .text_size(text_size)
- .font_weight(font_weight)
- .child(children.render(node_cx, window, cx))
- .into_any_element()
- }
- Node::Blockquote { children } => div()
- .id("blockquote")
- .w_full()
- .mb(mb)
- .text_color(cx.theme().muted_foreground)
- .border_l_3()
- .border_color(cx.theme().secondary_active)
- .px_4()
- .children({
- let children_len = children.len();
- children.into_iter().enumerate().map(move |(index, c)| {
- let is_last_child = is_root && index == children_len - 1;
- c.render(None, false, is_last_child, node_cx, window, cx)
- })
- })
- .into_any_element(),
- Node::List { children, ordered } => v_flex()
- .id(if *ordered { "ol" } else { "ul" })
- .mb(mb)
- .children({
- let mut items = Vec::with_capacity(children.len());
- let list_state = list_state.unwrap_or_default();
- let mut ix = 0;
- for item in children.into_iter() {
- let is_item = item.is_list_item();
-
- items.push(Self::render_list_item(
- item,
- ix,
- ListState {
- ordered: *ordered,
- todo: list_state.todo,
- depth: list_state.depth,
- },
- node_cx,
- window,
- cx,
- ));
-
- if is_item {
- ix += 1;
- }
- }
- items
- })
- .into_any_element(),
- Node::CodeBlock(code_block) => code_block.render(node_cx, window, cx),
- Node::Table { .. } => Self::render_table(self, node_cx, window, cx).into_any_element(),
- Node::Divider => div()
- .id("divider")
- .bg(cx.theme().border)
- .h(px(2.))
- .mb(mb)
- .into_any_element(),
- Node::Break { .. } => div().id("break").into_any_element(),
- Node::Unknown | Node::Definition { .. } => div().into_any_element(),
- _ => {
- if cfg!(debug_assertions) {
- tracing::warn!("unknown implementation: {:?}", self);
- }
-
- div().into_any_element()
- }
- }
+impl NodeRenderOptions {
+ fn is_last(mut self, is_last: bool) -> Self {
+ self.is_last = is_last;
+ self
}
}
@@ -1166,3 +857,374 @@ impl Node {
.to_string()
}
}
+
+impl Node {
+ fn render_list_item(
+ item: &Node,
+ ix: usize,
+ options: NodeRenderOptions,
+ node_cx: &NodeContext,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> impl IntoElement {
+ match item {
+ Node::ListItem {
+ children,
+ spread,
+ checked,
+ } => v_flex()
+ .id("li")
+ .when(*spread, |this| this.child(div()))
+ .children({
+ let mut items: Vec = Vec::with_capacity(children.len());
+
+ for (child_ix, child) in children.iter().enumerate() {
+ match child {
+ Node::Paragraph(_) => {
+ let last_not_list = child_ix > 0
+ && !matches!(children[child_ix - 1], Node::List { .. });
+
+ let text = child.render_block(
+ NodeRenderOptions {
+ depth: options.depth + 1,
+ todo: checked.is_some(),
+ is_last: true,
+ ..options
+ },
+ node_cx,
+ window,
+ cx,
+ );
+
+ // merge content into last item.
+ if last_not_list {
+ if let Some(item_item) = items.last_mut() {
+ item_item.extend(vec![div()
+ .overflow_hidden()
+ .child(text)
+ .into_any_element()]);
+ continue;
+ }
+ }
+
+ items.push(
+ h_flex()
+ .flex_1()
+ .relative()
+ .items_start()
+ .content_start()
+ .when(!options.todo && checked.is_none(), |this| {
+ this.child(list_item_prefix(
+ ix,
+ options.ordered,
+ options.depth,
+ ))
+ })
+ .when_some(*checked, |this, checked| {
+ // Todo list checkbox
+ this.child(
+ div()
+ .flex()
+ .mt(rems(0.4))
+ .mr_1p5()
+ .size(rems(0.875))
+ .items_center()
+ .justify_center()
+ .rounded(cx.theme().radius.half())
+ .border_1()
+ .border_color(cx.theme().primary)
+ .text_color(cx.theme().primary_foreground)
+ .when(checked, |this| {
+ this.bg(cx.theme().primary).child(
+ Icon::new(IconName::Check)
+ .size_2()
+ .text_xs(),
+ )
+ }),
+ )
+ })
+ .child(div().overflow_hidden().child(text)),
+ );
+ }
+ Node::List { .. } => {
+ items.push(div().ml(rems(1.)).child(child.render_block(
+ NodeRenderOptions {
+ depth: options.depth + 1,
+ todo: checked.is_some(),
+ is_last: true,
+ ..options
+ },
+ node_cx,
+ window,
+ cx,
+ )));
+ }
+ _ => {}
+ }
+ }
+ items
+ })
+ .into_any_element(),
+ _ => div().into_any_element(),
+ }
+ }
+
+ fn render_table(
+ item: &Node,
+ node_cx: &NodeContext,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> impl IntoElement {
+ const DEFAULT_LENGTH: usize = 5;
+ const MAX_LENGTH: usize = 150;
+ let col_lens = match item {
+ Node::Table(table) => {
+ let mut col_lens = vec![];
+ for row in table.children.iter() {
+ for (ix, cell) in row.children.iter().enumerate() {
+ if col_lens.len() <= ix {
+ col_lens.push(DEFAULT_LENGTH);
+ }
+
+ let len = cell.children.text_len();
+ if len > col_lens[ix] {
+ col_lens[ix] = len;
+ }
+ }
+ }
+ col_lens
+ }
+ _ => vec![],
+ };
+
+ match item {
+ Node::Table(table) => div()
+ .pb(rems(1.))
+ .w_full()
+ .child(
+ div()
+ .id("table")
+ .w_full()
+ .border_1()
+ .border_color(cx.theme().border)
+ .rounded(cx.theme().radius)
+ .children({
+ let mut rows = Vec::with_capacity(table.children.len());
+ for (row_ix, row) in table.children.iter().enumerate() {
+ rows.push(
+ div()
+ .id("row")
+ .w_full()
+ .when(row_ix < table.children.len() - 1, |this| {
+ this.border_b_1()
+ })
+ .border_color(cx.theme().border)
+ .flex()
+ .flex_row()
+ .children({
+ let mut cells = Vec::with_capacity(row.children.len());
+ for (ix, cell) in row.children.iter().enumerate() {
+ let align = table.column_align(ix);
+ let is_last_col = ix == row.children.len() - 1;
+ let len = col_lens
+ .get(ix)
+ .copied()
+ .unwrap_or(MAX_LENGTH)
+ .min(MAX_LENGTH);
+
+ cells.push(
+ div()
+ .id("cell")
+ .flex()
+ .when(
+ align == ColumnumnAlign::Center,
+ |this| this.justify_center(),
+ )
+ .when(
+ align == ColumnumnAlign::Right,
+ |this| this.justify_end(),
+ )
+ .w(Length::Definite(relative(len as f32)))
+ .px_2()
+ .py_1()
+ .when(!is_last_col, |this| {
+ this.border_r_1()
+ .border_color(cx.theme().border)
+ })
+ .truncate()
+ .child(
+ cell.children
+ .render(node_cx, window, cx),
+ ),
+ )
+ }
+ cells
+ }),
+ )
+ }
+ rows
+ }),
+ )
+ .into_any_element(),
+ _ => div().into_any_element(),
+ }
+ }
+
+ pub(super) fn render_root(
+ &self,
+ list_state: Option
,
+ node_cx: &NodeContext,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> impl IntoElement {
+ let options = NodeRenderOptions {
+ is_last: true,
+ ..Default::default()
+ };
+
+ let Some(list_state) = list_state else {
+ return self
+ .render_block(options, node_cx, window, cx)
+ .into_any_element();
+ };
+
+ let children = match self {
+ Node::Root { children } => children,
+ _ => return div().into_any_element(),
+ };
+
+ let children = children.clone();
+ let node_cx = node_cx.clone();
+
+ if list_state.item_count() != children.len() {
+ list_state.reset(children.len());
+ }
+
+ gpui::list(list_state, move |ix, window, cx| {
+ let is_last = ix + 1 == children.len();
+ children[ix]
+ .render_block(options.is_last(is_last), &node_cx, window, cx)
+ .into_any_element()
+ })
+ .size_full()
+ .into_any()
+ }
+
+ fn render_block(
+ &self,
+ options: NodeRenderOptions,
+ node_cx: &NodeContext,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> impl IntoElement {
+ let mb = if options.in_list || options.is_last {
+ rems(0.)
+ } else {
+ node_cx.style.paragraph_gap
+ };
+
+ match self {
+ Node::Root { children } => div()
+ .id("div")
+ .children(
+ children
+ .into_iter()
+ .map(move |node| node.render_block(options, node_cx, window, cx)),
+ )
+ .into_any_element(),
+ Node::Paragraph(paragraph) => div()
+ .id("p")
+ .pb(mb)
+ .child(paragraph.render(node_cx, window, cx))
+ .into_any_element(),
+ Node::Heading { level, children } => {
+ let (text_size, font_weight) = match level {
+ 1 => (rems(2.), FontWeight::BOLD),
+ 2 => (rems(1.5), FontWeight::SEMIBOLD),
+ 3 => (rems(1.25), FontWeight::SEMIBOLD),
+ 4 => (rems(1.125), FontWeight::SEMIBOLD),
+ 5 => (rems(1.), FontWeight::SEMIBOLD),
+ 6 => (rems(1.), FontWeight::MEDIUM),
+ _ => (rems(1.), FontWeight::NORMAL),
+ };
+
+ let mut text_size = text_size.to_pixels(node_cx.style.heading_base_font_size);
+ if let Some(f) = node_cx.style.heading_font_size.as_ref() {
+ text_size = (f)(*level, node_cx.style.heading_base_font_size);
+ }
+
+ h_flex()
+ .id(("h", *level as usize))
+ .pb(rems(0.3))
+ .whitespace_normal()
+ .text_size(text_size)
+ .font_weight(font_weight)
+ .child(children.render(node_cx, window, cx))
+ .into_any_element()
+ }
+ Node::Blockquote { children } => div()
+ .w_full()
+ .pb(mb)
+ .child(
+ div()
+ .id("blockquote")
+ .w_full()
+ .text_color(cx.theme().muted_foreground)
+ .border_l_3()
+ .border_color(cx.theme().secondary_active)
+ .px_4()
+ .children({
+ let children_len = children.len();
+ children.into_iter().enumerate().map(move |(index, c)| {
+ let is_last = index == children_len - 1;
+ c.render_block(options.is_last(is_last), node_cx, window, cx)
+ })
+ }),
+ )
+ .into_any_element(),
+ Node::List { children, ordered } => v_flex()
+ .id(if *ordered { "ol" } else { "ul" })
+ .pb(mb)
+ .children({
+ let mut items = Vec::with_capacity(children.len());
+ let mut ix = 0;
+ for item in children.into_iter() {
+ let is_item = item.is_list_item();
+
+ items.push(Self::render_list_item(
+ item,
+ ix,
+ NodeRenderOptions {
+ ordered: *ordered,
+ ..options
+ },
+ node_cx,
+ window,
+ cx,
+ ));
+
+ if is_item {
+ ix += 1;
+ }
+ }
+ items
+ })
+ .into_any_element(),
+ Node::CodeBlock(code_block) => code_block.render(&options, node_cx, window, cx),
+ Node::Table { .. } => Self::render_table(self, node_cx, window, cx).into_any_element(),
+ Node::Divider => div()
+ .pb(mb)
+ .child(div().id("divider").bg(cx.theme().border).h(px(2.)))
+ .into_any_element(),
+ Node::Break { .. } => div().id("break").into_any_element(),
+ Node::Unknown | Node::Definition { .. } => div().into_any_element(),
+ _ => {
+ if cfg!(debug_assertions) {
+ tracing::warn!("unknown implementation: {:?}", self);
+ }
+
+ div().into_any_element()
+ }
+ }
+ }
+}
diff --git a/crates/ui/src/text/text_view.rs b/crates/ui/src/text/text_view.rs
index aad99c1b..98efdbc0 100644
--- a/crates/ui/src/text/text_view.rs
+++ b/crates/ui/src/text/text_view.rs
@@ -6,14 +6,16 @@ use std::time::Duration;
use gpui::prelude::FluentBuilder;
use gpui::{
- div, AnyElement, App, AppContext, Bounds, ClipboardItem, Context, Element, ElementId, Entity,
- EntityId, FocusHandle, GlobalElementId, InspectorElementId, InteractiveElement, IntoElement,
- KeyBinding, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels,
- Point, RenderOnce, SharedString, Size, Styled, Timer, Window,
+ div, px, AnyElement, App, AppContext, Bounds, ClipboardItem, Context, Element, ElementId,
+ Entity, EntityId, FocusHandle, GlobalElementId, InspectorElementId, InteractiveElement,
+ IntoElement, KeyBinding, LayoutId, ListState, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
+ ParentElement, Pixels, Point, RenderOnce, SharedString, Size, StyleRefinement, Styled, Timer,
+ Window,
};
use smol::stream::StreamExt;
use crate::highlighter::HighlightTheme;
+use crate::scroll::{Scrollbar, ScrollbarState};
use crate::{
global_state::GlobalState,
input::{self},
@@ -22,7 +24,7 @@ use crate::{
TextViewStyle,
},
};
-use crate::{v_flex, ActiveTheme};
+use crate::{v_flex, ActiveTheme, StyledExt};
const CONTEXT: &'static str = "TextView";
@@ -37,29 +39,30 @@ pub(crate) fn init(cx: &mut App) {
#[derive(IntoElement, Clone)]
struct TextViewElement {
+ list_state: Option,
state: Entity,
}
impl RenderOnce for TextViewElement {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
self.state.update(cx, |state, cx| {
- div().map(|this| match &mut state.parsed_result {
- Some(Ok(content)) => this.child(content.root_node.render(
- None,
- true,
- true,
- &content.node_cx,
- window,
- cx,
- )),
- Some(Err(err)) => this.child(
- v_flex()
- .gap_1()
- .child("Failed to parse content")
- .child(err.to_string()),
- ),
- None => this,
- })
+ v_flex()
+ .size_full()
+ .map(|this| match &mut state.parsed_result {
+ Some(Ok(content)) => this.child(content.root_node.render_root(
+ self.list_state.clone(),
+ &content.node_cx,
+ window,
+ cx,
+ )),
+ Some(Err(err)) => this.child(
+ v_flex()
+ .gap_1()
+ .child("Failed to parse content")
+ .child(err.to_string()),
+ ),
+ None => this,
+ })
})
}
}
@@ -85,7 +88,9 @@ pub struct TextView {
id: ElementId,
init_state: Option,
state: Entity,
+ style: StyleRefinement,
selectable: bool,
+ scrollable: bool,
}
#[derive(PartialEq)]
@@ -205,7 +210,6 @@ pub(crate) struct TextViewState {
tx: Option>,
parsed_result: Option>,
focus_handle: Option,
-
/// The bounds of the text view
bounds: Bounds,
/// The local (in TextView) position of the selection.
@@ -213,6 +217,8 @@ pub(crate) struct TextViewState {
/// Is current in selection.
is_selecting: bool,
is_selectable: bool,
+ scrollbar_state: ScrollbarState,
+ list_state: ListState,
}
impl TextViewState {
@@ -227,6 +233,8 @@ impl TextViewState {
selection_positions: (None, None),
is_selecting: false,
is_selectable: false,
+ scrollbar_state: ScrollbarState::default(),
+ list_state: ListState::new(0, gpui::ListAlignment::Top, px(1000.)),
}
}
}
@@ -346,6 +354,12 @@ impl RenderOnce for Text {
}
}
+impl Styled for TextView {
+ fn style(&mut self) -> &mut StyleRefinement {
+ &mut self.style
+ }
+}
+
impl TextView {
fn create_init_state(
type_: TextViewType,
@@ -394,8 +408,10 @@ impl TextView {
Self {
id,
init_state: Some(init_state),
+ style: StyleRefinement::default(),
state,
selectable: false,
+ scrollable: false,
}
}
@@ -421,8 +437,10 @@ impl TextView {
Self {
id,
init_state: Some(init_state),
+ style: StyleRefinement::default(),
state,
selectable: false,
+ scrollable: false,
}
}
@@ -459,6 +477,23 @@ impl TextView {
self
}
+ /// Set the text view to be scrollable, default is false.
+ ///
+ /// ## If true for `scrollable`
+ ///
+ /// The `scrollable` mode used for large content,
+ /// will show scrollbar, but requires the parent to have a fixed height,
+ /// and use [`gpui::list`] to render the content in a virtualized way.
+ ///
+ /// ## If false to fit content
+ ///
+ /// The TextView will expand to fit all content, no scrollbar.
+ /// This mode is suitable for small content, such as a few lines of text, a label, etc.
+ pub fn scrollable(mut self) -> Self {
+ self.scrollable = true;
+ self
+ }
+
fn on_action_copy(state: &Entity, cx: &mut App) {
let Some(selected_text) = state.read(cx).selection_text() else {
return;
@@ -553,6 +588,9 @@ impl Element for TextView {
self.init_state = Some(InitState::Initialized { tx });
}
+ let scrollbar_state = &self.state.read(cx).scrollbar_state;
+ let list_state = &self.state.read(cx).list_state;
+
let focus_handle = self
.state
.read(cx)
@@ -563,6 +601,8 @@ impl Element for TextView {
let mut el = div()
.key_context(CONTEXT)
.track_focus(focus_handle)
+ .size_full()
+ .relative()
.on_action({
let state = self.state.clone();
move |_: &input::Copy, _, cx| {
@@ -570,8 +610,25 @@ impl Element for TextView {
}
})
.child(TextViewElement {
+ list_state: if self.scrollable {
+ Some(list_state.clone())
+ } else {
+ None
+ },
state: self.state.clone(),
})
+ .refine_style(&self.style)
+ .when(self.scrollable, |this| {
+ this.child(
+ div()
+ .absolute()
+ .w(Scrollbar::width())
+ .top_0()
+ .right_0()
+ .bottom_0()
+ .child(Scrollbar::vertical(scrollbar_state, list_state)),
+ )
+ })
.into_any_element();
let layout_id = el.request_layout(window, cx);
(layout_id, el)
@@ -636,7 +693,11 @@ impl Element for TextView {
// move to update end position.
window.on_mouse_event({
let state = self.state.clone();
- move |event: &MouseMoveEvent, _, _, cx| {
+ move |event: &MouseMoveEvent, phase, _, cx| {
+ if !phase.bubble() {
+ return;
+ }
+
state.update(cx, |state, _| {
state.update_selection(event.position);
});
@@ -647,7 +708,11 @@ impl Element for TextView {
// up to end selection
window.on_mouse_event({
let state = self.state.clone();
- move |_: &MouseUpEvent, _, _, cx| {
+ move |_: &MouseUpEvent, phase, _, cx| {
+ if !phase.bubble() {
+ return;
+ }
+
state.update(cx, |state, _| {
state.end_selection();
});