markdown: Use list for Markdown render and add scrollbar. (#1357)
This commit is contained in:
parent
2574e7f812
commit
5b2fed0ccb
9 changed files with 594 additions and 425 deletions
|
|
@ -2,7 +2,7 @@ use gpui::*;
|
|||
use gpui_component::{
|
||||
highlighter::Language,
|
||||
input::{InputState, TabSize, TextInput},
|
||||
resizable::{h_resizable, resizable_panel, ResizableState},
|
||||
resizable::{ResizableState, h_resizable, resizable_panel},
|
||||
text::TextView,
|
||||
};
|
||||
use story::Assets;
|
||||
|
|
@ -69,20 +69,15 @@ impl Render for Example {
|
|||
)
|
||||
.child(
|
||||
resizable_panel().child(
|
||||
div()
|
||||
.id("preview")
|
||||
.size_full()
|
||||
.p_5()
|
||||
.overflow_y_scroll()
|
||||
.child(
|
||||
TextView::html(
|
||||
"preview",
|
||||
self.input_state.read(cx).value().clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.selectable(),
|
||||
),
|
||||
TextView::html(
|
||||
"preview",
|
||||
self.input_state.read(cx).value().clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.p_5()
|
||||
.scrollable()
|
||||
.selectable(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,20 +98,16 @@ impl Render for Example {
|
|||
)
|
||||
.child(
|
||||
resizable_panel().child(
|
||||
div()
|
||||
.id("preview")
|
||||
.size_full()
|
||||
.p_5()
|
||||
.overflow_y_scroll()
|
||||
.child(
|
||||
TextView::markdown(
|
||||
"preview",
|
||||
self.input_state.read(cx).value().clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.selectable(),
|
||||
),
|
||||
TextView::markdown(
|
||||
"preview",
|
||||
self.input_state.read(cx).value().clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.flex_none()
|
||||
.p_5()
|
||||
.scrollable()
|
||||
.selectable(),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -459,6 +459,7 @@ pub struct StoryContainer {
|
|||
story_klass: Option<SharedString>,
|
||||
closable: bool,
|
||||
zoomable: Option<PanelControl>,
|
||||
paddings: Pixels,
|
||||
on_active: Option<fn(AnyView, bool, &mut Window, &mut App)>,
|
||||
}
|
||||
|
||||
|
|
@ -473,18 +474,27 @@ pub trait Story: Render + Sized {
|
|||
}
|
||||
|
||||
fn title() -> &'static str;
|
||||
|
||||
fn description() -> &'static str {
|
||||
""
|
||||
}
|
||||
|
||||
fn closable() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn zoomable() -> Option<PanelControl> {
|
||||
Some(PanelControl::default())
|
||||
}
|
||||
|
||||
fn title_bg() -> Option<Hsla> {
|
||||
None
|
||||
}
|
||||
|
||||
fn paddings() -> Pixels {
|
||||
px(16.)
|
||||
}
|
||||
|
||||
fn new_view(window: &mut Window, cx: &mut App) -> Entity<impl Render>;
|
||||
|
||||
fn on_active(&mut self, active: bool, window: &mut Window, cx: &mut App) {
|
||||
|
|
@ -492,6 +502,7 @@ pub trait Story: Render + Sized {
|
|||
let _ = window;
|
||||
let _ = cx;
|
||||
}
|
||||
|
||||
fn on_active_any(view: AnyView, active: bool, window: &mut Window, cx: &mut App)
|
||||
where
|
||||
Self: 'static,
|
||||
|
|
@ -521,6 +532,7 @@ impl StoryContainer {
|
|||
story_klass: None,
|
||||
closable: true,
|
||||
zoomable: Some(PanelControl::default()),
|
||||
paddings: px(16.),
|
||||
on_active: None,
|
||||
}
|
||||
}
|
||||
|
|
@ -541,6 +553,7 @@ impl StoryContainer {
|
|||
story.name = name.into();
|
||||
story.description = description.into();
|
||||
story.title_bg = S::title_bg();
|
||||
story.paddings = S::paddings();
|
||||
story
|
||||
});
|
||||
|
||||
|
|
@ -768,7 +781,7 @@ impl Render for StoryContainer {
|
|||
.id("story-children")
|
||||
.w_full()
|
||||
.flex_1()
|
||||
.p_4()
|
||||
.p(self.paddings)
|
||||
.child(story),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use gpui::{
|
||||
App, AppContext, Context, Entity, FocusHandle, Focusable, ParentElement, Render, Styled, Window,
|
||||
App, AppContext, Context, Entity, FocusHandle, Focusable, Render, Styled as _, Window, px,
|
||||
};
|
||||
|
||||
use gpui_component::{dock::PanelControl, text::TextView, v_flex};
|
||||
use gpui_component::{dock::PanelControl, text::TextView};
|
||||
|
||||
use crate::Story;
|
||||
|
||||
|
|
@ -38,6 +38,10 @@ impl Story for WelcomeStory {
|
|||
fn zoomable() -> Option<PanelControl> {
|
||||
None
|
||||
}
|
||||
|
||||
fn paddings() -> gpui::Pixels {
|
||||
px(0.)
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for WelcomeStory {
|
||||
|
|
@ -52,9 +56,9 @@ impl Render for WelcomeStory {
|
|||
window: &mut gpui::Window,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) -> impl gpui::IntoElement {
|
||||
v_flex().p_4().gap_5().child(
|
||||
TextView::markdown("intro", include_str!("../../../README.md"), window, cx)
|
||||
.selectable(),
|
||||
)
|
||||
TextView::markdown("intro", include_str!("../../../README.md"), window, cx)
|
||||
.p_4()
|
||||
.scrollable()
|
||||
.selectable()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ use std::{ops::Range, rc::Rc};
|
|||
use gpui::{
|
||||
deferred, div, point, prelude::FluentBuilder as _, px, AnyElement, App, AppContext as _,
|
||||
AvailableSpace, Bounds, Element, ElementId, Entity, InteractiveElement, IntoElement,
|
||||
MouseDownEvent, ParentElement as _, Pixels, Render, StyleRefinement, Styled, Window,
|
||||
MouseDownEvent, ParentElement as _, Pixels, Render, StatefulInteractiveElement as _,
|
||||
StyleRefinement, Styled, Window,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
|
|
@ -184,11 +185,13 @@ impl Element for Popover {
|
|||
.end
|
||||
.min(window.bounds().size.width - SNAP_TO_EDGE * 2)
|
||||
.max(px(200.));
|
||||
let max_height = (window.bounds().size.height - SNAP_TO_EDGE * 2).min(px(320.));
|
||||
|
||||
let is_open = *open_state.read(cx);
|
||||
|
||||
let mut popover = deferred(
|
||||
div()
|
||||
.id("hover-popover-content")
|
||||
.when(!is_open, |s| s.invisible())
|
||||
.flex_none()
|
||||
.occlude()
|
||||
|
|
@ -197,6 +200,8 @@ impl Element for Popover {
|
|||
.popover_style(cx)
|
||||
.shadow_md()
|
||||
.max_w(max_width)
|
||||
.max_h(max_height)
|
||||
.overflow_y_scroll()
|
||||
.refine_style(&self.style)
|
||||
.child((self.content_builder)(window, cx)),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ use crate::{ActiveTheme, AxisExt};
|
|||
use gpui::{
|
||||
fill, point, px, relative, size, App, Axis, BorderStyle, Bounds, ContentMask, Corner,
|
||||
CursorStyle, Edges, Element, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InspectorElementId,
|
||||
IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point,
|
||||
Position, ScrollHandle, ScrollWheelEvent, Size, Style, Timer, UniformListScrollHandle, Window,
|
||||
IntoElement, LayoutId, ListState, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
|
||||
Pixels, Point, Position, ScrollHandle, ScrollWheelEvent, Size, Style, Timer,
|
||||
UniformListScrollHandle, Window,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -35,7 +36,7 @@ impl ScrollbarShow {
|
|||
}
|
||||
|
||||
/// The width of the scrollbar (THUMB_ACTIVE_INSET * 2 + THUMB_ACTIVE_WIDTH)
|
||||
pub(crate) const WIDTH: Pixels = px(2. * 2. + 8.);
|
||||
const WIDTH: Pixels = px(2. * 2. + 8.);
|
||||
const MIN_THUMB_SIZE: f32 = 48.;
|
||||
|
||||
const THUMB_WIDTH: Pixels = px(6.);
|
||||
|
|
@ -52,11 +53,10 @@ const FADE_OUT_DELAY: f32 = 2.0;
|
|||
pub trait ScrollHandleOffsetable {
|
||||
fn offset(&self) -> Point<Pixels>;
|
||||
fn set_offset(&self, offset: Point<Pixels>);
|
||||
fn is_uniform_list(&self) -> bool {
|
||||
false
|
||||
}
|
||||
/// The full size of the content, including padding.
|
||||
fn content_size(&self) -> Size<Pixels>;
|
||||
fn start_drag(&self) {}
|
||||
fn end_drag(&self) {}
|
||||
}
|
||||
|
||||
impl ScrollHandleOffsetable for ScrollHandle {
|
||||
|
|
@ -82,16 +82,34 @@ impl ScrollHandleOffsetable for UniformListScrollHandle {
|
|||
self.0.borrow_mut().base_handle.set_offset(offset)
|
||||
}
|
||||
|
||||
fn is_uniform_list(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Size<Pixels> {
|
||||
let base_handle = &self.0.borrow().base_handle;
|
||||
base_handle.max_offset() + base_handle.bounds().size
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollHandleOffsetable for ListState {
|
||||
fn offset(&self) -> Point<Pixels> {
|
||||
self.scroll_px_offset_for_scrollbar()
|
||||
}
|
||||
|
||||
fn set_offset(&self, offset: Point<Pixels>) {
|
||||
self.set_offset_from_scrollbar(offset);
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Size<Pixels> {
|
||||
self.viewport_bounds().size + self.max_offset_for_scrollbar()
|
||||
}
|
||||
|
||||
fn start_drag(&self) {
|
||||
self.scrollbar_drag_started();
|
||||
}
|
||||
|
||||
fn end_drag(&self) {
|
||||
self.scrollbar_drag_ended();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScrollbarState(Rc<Cell<ScrollbarStateInner>>);
|
||||
|
||||
|
|
@ -296,6 +314,11 @@ impl Scrollbar {
|
|||
}
|
||||
}
|
||||
|
||||
// Get the width of the scrollbar.
|
||||
pub(crate) const fn width() -> Pixels {
|
||||
WIDTH
|
||||
}
|
||||
|
||||
/// Create with vertical and horizontal scrollbar.
|
||||
pub fn both(
|
||||
state: &ScrollbarState,
|
||||
|
|
@ -778,6 +801,7 @@ impl Element for Scrollbar {
|
|||
// click on the thumb bar, set the drag position
|
||||
let pos = event.position - thumb_bounds.origin;
|
||||
|
||||
scroll_handle.start_drag();
|
||||
state.set(state.get().with_drag_pos(axis, pos));
|
||||
|
||||
cx.notify(view_id);
|
||||
|
|
@ -854,6 +878,9 @@ impl Element for Scrollbar {
|
|||
|
||||
// Move thumb position on dragging
|
||||
if state.get().dragged_axis == Some(axis) && event.dragging() {
|
||||
// Stop the event propagation to avoid selecting text or other side effects.
|
||||
cx.stop_propagation();
|
||||
|
||||
// drag_pos is the position of the mouse down event
|
||||
// We need to keep the thumb bar still at the origin down position
|
||||
let drag_pos = state.get().drag_pos;
|
||||
|
|
@ -900,10 +927,12 @@ impl Element for Scrollbar {
|
|||
});
|
||||
|
||||
window.on_mouse_event({
|
||||
let scroll_handle = self.scroll_handle.clone();
|
||||
let state = self.state.clone();
|
||||
|
||||
move |_event: &MouseUpEvent, phase, _, cx| {
|
||||
if phase.bubble() {
|
||||
scroll_handle.end_drag();
|
||||
state.set(state.get().with_unset_drag_pos());
|
||||
cx.notify(view_id);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use crate::{
|
|||
context_menu::ContextMenuExt,
|
||||
h_flex,
|
||||
popup_menu::PopupMenu,
|
||||
scroll::{self, ScrollableMask, Scrollbar, ScrollbarState},
|
||||
scroll::{ScrollableMask, Scrollbar, ScrollbarState},
|
||||
v_flex, ActiveTheme, Icon, IconName, Sizable, Size, StyleSized as _, StyledExt,
|
||||
VirtualListScrollHandle,
|
||||
};
|
||||
|
|
@ -688,7 +688,7 @@ where
|
|||
.top(self.size.table_row_height())
|
||||
.right_0()
|
||||
.bottom_0()
|
||||
.w(scroll::WIDTH)
|
||||
.w(Scrollbar::width())
|
||||
.on_scroll_wheel(cx.listener(|_, _: &ScrollWheelEvent, _, cx| {
|
||||
cx.notify();
|
||||
}))
|
||||
|
|
@ -709,7 +709,7 @@ where
|
|||
.left(self.fixed_head_cols_bounds.size.width)
|
||||
.right_0()
|
||||
.bottom_0()
|
||||
.h(scroll::WIDTH)
|
||||
.h(Scrollbar::width())
|
||||
.on_scroll_wheel(cx.listener(|_, _: &ScrollWheelEvent, _, cx| {
|
||||
cx.notify();
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ use std::{
|
|||
|
||||
use gpui::{
|
||||
div, img, prelude::FluentBuilder as _, px, relative, rems, AnyElement, App, DefiniteLength,
|
||||
Div, ElementId, FontStyle, FontWeight, Half, HighlightStyle, InteractiveElement as _,
|
||||
IntoElement, Length, ObjectFit, ParentElement, SharedString, SharedUri,
|
||||
Div, Element, ElementId, FontStyle, FontWeight, Half, HighlightStyle, InteractiveElement as _,
|
||||
IntoElement, Length, ListState, ObjectFit, ParentElement, SharedString, SharedUri,
|
||||
StatefulInteractiveElement, Styled, StyledImage as _, Window,
|
||||
};
|
||||
use markdown::mdast;
|
||||
|
|
@ -111,7 +111,7 @@ impl PartialEq for ImageNode {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub(crate) struct InlineNode {
|
||||
/// The text content.
|
||||
pub(crate) text: SharedString,
|
||||
|
|
@ -154,7 +154,7 @@ impl InlineNode {
|
|||
///
|
||||
/// Unlike other Element, this is cloneable, because it is used in the Node AST.
|
||||
/// We are keep the selection state inside this AST Nodes.
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub(crate) struct Paragraph {
|
||||
pub(super) span: Option<Span>,
|
||||
pub(super) children: Vec<InlineNode>,
|
||||
|
|
@ -205,7 +205,7 @@ impl Paragraph {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub(crate) struct Table {
|
||||
pub children: Vec<TableRow>,
|
||||
pub column_aligns: Vec<ColumnumnAlign>,
|
||||
|
|
@ -236,12 +236,12 @@ impl From<mdast::AlignKind> for ColumnumnAlign {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub(crate) struct TableRow {
|
||||
pub children: Vec<TableCell>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub(crate) struct TableCell {
|
||||
pub children: Paragraph,
|
||||
pub width: Option<DefiniteLength>,
|
||||
|
|
@ -354,25 +354,34 @@ impl CodeBlock {
|
|||
text
|
||||
}
|
||||
|
||||
fn render(&self, node_cx: &NodeContext, _: &mut Window, cx: &mut App) -> AnyElement {
|
||||
fn render(
|
||||
&self,
|
||||
options: &NodeRenderOptions,
|
||||
node_cx: &NodeContext,
|
||||
_: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
let style = &node_cx.style;
|
||||
|
||||
div()
|
||||
.id("codeblock")
|
||||
.mb(style.paragraph_gap)
|
||||
.p_3()
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().secondary.opacity(0.85))
|
||||
.font_family("Menlo, Monaco, Consolas, monospace")
|
||||
.text_size(rems(0.875))
|
||||
.relative()
|
||||
.refine_style(&style.code_block)
|
||||
.child(Inline::new(
|
||||
"code",
|
||||
self.state.clone(),
|
||||
vec![],
|
||||
self.styles.clone(),
|
||||
))
|
||||
.when(!options.is_last, |this| this.pb(style.paragraph_gap))
|
||||
.child(
|
||||
div()
|
||||
.id("codeblock")
|
||||
.p_3()
|
||||
.rounded(cx.theme().radius)
|
||||
.bg(cx.theme().secondary.opacity(0.85))
|
||||
.font_family("Menlo, Monaco, Consolas, monospace")
|
||||
.text_size(rems(0.875))
|
||||
.relative()
|
||||
.refine_style(&style.code_block)
|
||||
.child(Inline::new(
|
||||
"code",
|
||||
self.state.clone(),
|
||||
vec![],
|
||||
self.styles.clone(),
|
||||
)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
|
@ -391,7 +400,7 @@ impl NodeContext {
|
|||
}
|
||||
|
||||
/// The AST Node of the rich text.
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) enum Node {
|
||||
Root {
|
||||
children: Vec<Node>,
|
||||
|
|
@ -651,337 +660,19 @@ impl Paragraph {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct ListState {
|
||||
#[derive(Default, Clone, Copy)]
|
||||
struct NodeRenderOptions {
|
||||
in_list: bool,
|
||||
todo: bool,
|
||||
ordered: bool,
|
||||
depth: usize,
|
||||
is_last: bool,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
fn render_list_item(
|
||||
item: &Node,
|
||||
ix: usize,
|
||||
state: ListState,
|
||||
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<Div> = 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<ListState>,
|
||||
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<Div> = 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<ListState>,
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ListState>,
|
||||
state: Entity<TextViewState>,
|
||||
}
|
||||
|
||||
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<InitState>,
|
||||
state: Entity<TextViewState>,
|
||||
style: StyleRefinement,
|
||||
selectable: bool,
|
||||
scrollable: bool,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
|
|
@ -205,7 +210,6 @@ pub(crate) struct TextViewState {
|
|||
tx: Option<smol::channel::Sender<Update>>,
|
||||
parsed_result: Option<Result<ParsedContent, SharedString>>,
|
||||
focus_handle: Option<FocusHandle>,
|
||||
|
||||
/// The bounds of the text view
|
||||
bounds: Bounds<Pixels>,
|
||||
/// 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<TextViewState>, 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();
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue