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::{
|
use gpui_component::{
|
||||||
highlighter::Language,
|
highlighter::Language,
|
||||||
input::{InputState, TabSize, TextInput},
|
input::{InputState, TabSize, TextInput},
|
||||||
resizable::{h_resizable, resizable_panel, ResizableState},
|
resizable::{ResizableState, h_resizable, resizable_panel},
|
||||||
text::TextView,
|
text::TextView,
|
||||||
};
|
};
|
||||||
use story::Assets;
|
use story::Assets;
|
||||||
|
|
@ -69,20 +69,15 @@ impl Render for Example {
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
resizable_panel().child(
|
resizable_panel().child(
|
||||||
div()
|
TextView::html(
|
||||||
.id("preview")
|
"preview",
|
||||||
.size_full()
|
self.input_state.read(cx).value().clone(),
|
||||||
.p_5()
|
window,
|
||||||
.overflow_y_scroll()
|
cx,
|
||||||
.child(
|
)
|
||||||
TextView::html(
|
.p_5()
|
||||||
"preview",
|
.scrollable()
|
||||||
self.input_state.read(cx).value().clone(),
|
.selectable(),
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.selectable(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -98,20 +98,16 @@ impl Render for Example {
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
resizable_panel().child(
|
resizable_panel().child(
|
||||||
div()
|
TextView::markdown(
|
||||||
.id("preview")
|
"preview",
|
||||||
.size_full()
|
self.input_state.read(cx).value().clone(),
|
||||||
.p_5()
|
window,
|
||||||
.overflow_y_scroll()
|
cx,
|
||||||
.child(
|
)
|
||||||
TextView::markdown(
|
.flex_none()
|
||||||
"preview",
|
.p_5()
|
||||||
self.input_state.read(cx).value().clone(),
|
.scrollable()
|
||||||
window,
|
.selectable(),
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.selectable(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -459,6 +459,7 @@ pub struct StoryContainer {
|
||||||
story_klass: Option<SharedString>,
|
story_klass: Option<SharedString>,
|
||||||
closable: bool,
|
closable: bool,
|
||||||
zoomable: Option<PanelControl>,
|
zoomable: Option<PanelControl>,
|
||||||
|
paddings: Pixels,
|
||||||
on_active: Option<fn(AnyView, bool, &mut Window, &mut App)>,
|
on_active: Option<fn(AnyView, bool, &mut Window, &mut App)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -473,18 +474,27 @@ pub trait Story: Render + Sized {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn title() -> &'static str;
|
fn title() -> &'static str;
|
||||||
|
|
||||||
fn description() -> &'static str {
|
fn description() -> &'static str {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
fn closable() -> bool {
|
fn closable() -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
fn zoomable() -> Option<PanelControl> {
|
fn zoomable() -> Option<PanelControl> {
|
||||||
Some(PanelControl::default())
|
Some(PanelControl::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn title_bg() -> Option<Hsla> {
|
fn title_bg() -> Option<Hsla> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn paddings() -> Pixels {
|
||||||
|
px(16.)
|
||||||
|
}
|
||||||
|
|
||||||
fn new_view(window: &mut Window, cx: &mut App) -> Entity<impl Render>;
|
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) {
|
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 _ = window;
|
||||||
let _ = cx;
|
let _ = cx;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_active_any(view: AnyView, active: bool, window: &mut Window, cx: &mut App)
|
fn on_active_any(view: AnyView, active: bool, window: &mut Window, cx: &mut App)
|
||||||
where
|
where
|
||||||
Self: 'static,
|
Self: 'static,
|
||||||
|
|
@ -521,6 +532,7 @@ impl StoryContainer {
|
||||||
story_klass: None,
|
story_klass: None,
|
||||||
closable: true,
|
closable: true,
|
||||||
zoomable: Some(PanelControl::default()),
|
zoomable: Some(PanelControl::default()),
|
||||||
|
paddings: px(16.),
|
||||||
on_active: None,
|
on_active: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -541,6 +553,7 @@ impl StoryContainer {
|
||||||
story.name = name.into();
|
story.name = name.into();
|
||||||
story.description = description.into();
|
story.description = description.into();
|
||||||
story.title_bg = S::title_bg();
|
story.title_bg = S::title_bg();
|
||||||
|
story.paddings = S::paddings();
|
||||||
story
|
story
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -768,7 +781,7 @@ impl Render for StoryContainer {
|
||||||
.id("story-children")
|
.id("story-children")
|
||||||
.w_full()
|
.w_full()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.p_4()
|
.p(self.paddings)
|
||||||
.child(story),
|
.child(story),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
use gpui::{
|
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;
|
use crate::Story;
|
||||||
|
|
||||||
|
|
@ -38,6 +38,10 @@ impl Story for WelcomeStory {
|
||||||
fn zoomable() -> Option<PanelControl> {
|
fn zoomable() -> Option<PanelControl> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn paddings() -> gpui::Pixels {
|
||||||
|
px(0.)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Focusable for WelcomeStory {
|
impl Focusable for WelcomeStory {
|
||||||
|
|
@ -52,9 +56,9 @@ impl Render for WelcomeStory {
|
||||||
window: &mut gpui::Window,
|
window: &mut gpui::Window,
|
||||||
cx: &mut gpui::Context<Self>,
|
cx: &mut gpui::Context<Self>,
|
||||||
) -> impl gpui::IntoElement {
|
) -> impl gpui::IntoElement {
|
||||||
v_flex().p_4().gap_5().child(
|
TextView::markdown("intro", include_str!("../../../README.md"), window, cx)
|
||||||
TextView::markdown("intro", include_str!("../../../README.md"), window, cx)
|
.p_4()
|
||||||
.selectable(),
|
.scrollable()
|
||||||
)
|
.selectable()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@ use std::{ops::Range, rc::Rc};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
deferred, div, point, prelude::FluentBuilder as _, px, AnyElement, App, AppContext as _,
|
deferred, div, point, prelude::FluentBuilder as _, px, AnyElement, App, AppContext as _,
|
||||||
AvailableSpace, Bounds, Element, ElementId, Entity, InteractiveElement, IntoElement,
|
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::{
|
use crate::{
|
||||||
|
|
@ -184,11 +185,13 @@ impl Element for Popover {
|
||||||
.end
|
.end
|
||||||
.min(window.bounds().size.width - SNAP_TO_EDGE * 2)
|
.min(window.bounds().size.width - SNAP_TO_EDGE * 2)
|
||||||
.max(px(200.));
|
.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 is_open = *open_state.read(cx);
|
||||||
|
|
||||||
let mut popover = deferred(
|
let mut popover = deferred(
|
||||||
div()
|
div()
|
||||||
|
.id("hover-popover-content")
|
||||||
.when(!is_open, |s| s.invisible())
|
.when(!is_open, |s| s.invisible())
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.occlude()
|
.occlude()
|
||||||
|
|
@ -197,6 +200,8 @@ impl Element for Popover {
|
||||||
.popover_style(cx)
|
.popover_style(cx)
|
||||||
.shadow_md()
|
.shadow_md()
|
||||||
.max_w(max_width)
|
.max_w(max_width)
|
||||||
|
.max_h(max_height)
|
||||||
|
.overflow_y_scroll()
|
||||||
.refine_style(&self.style)
|
.refine_style(&self.style)
|
||||||
.child((self.content_builder)(window, cx)),
|
.child((self.content_builder)(window, cx)),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,9 @@ use crate::{ActiveTheme, AxisExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
fill, point, px, relative, size, App, Axis, BorderStyle, Bounds, ContentMask, Corner,
|
fill, point, px, relative, size, App, Axis, BorderStyle, Bounds, ContentMask, Corner,
|
||||||
CursorStyle, Edges, Element, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InspectorElementId,
|
CursorStyle, Edges, Element, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InspectorElementId,
|
||||||
IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point,
|
IntoElement, LayoutId, ListState, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
|
||||||
Position, ScrollHandle, ScrollWheelEvent, Size, Style, Timer, UniformListScrollHandle, Window,
|
Pixels, Point, Position, ScrollHandle, ScrollWheelEvent, Size, Style, Timer,
|
||||||
|
UniformListScrollHandle, Window,
|
||||||
};
|
};
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -35,7 +36,7 @@ impl ScrollbarShow {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The width of the scrollbar (THUMB_ACTIVE_INSET * 2 + THUMB_ACTIVE_WIDTH)
|
/// 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 MIN_THUMB_SIZE: f32 = 48.;
|
||||||
|
|
||||||
const THUMB_WIDTH: Pixels = px(6.);
|
const THUMB_WIDTH: Pixels = px(6.);
|
||||||
|
|
@ -52,11 +53,10 @@ const FADE_OUT_DELAY: f32 = 2.0;
|
||||||
pub trait ScrollHandleOffsetable {
|
pub trait ScrollHandleOffsetable {
|
||||||
fn offset(&self) -> Point<Pixels>;
|
fn offset(&self) -> Point<Pixels>;
|
||||||
fn set_offset(&self, offset: Point<Pixels>);
|
fn set_offset(&self, offset: Point<Pixels>);
|
||||||
fn is_uniform_list(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
/// The full size of the content, including padding.
|
/// The full size of the content, including padding.
|
||||||
fn content_size(&self) -> Size<Pixels>;
|
fn content_size(&self) -> Size<Pixels>;
|
||||||
|
fn start_drag(&self) {}
|
||||||
|
fn end_drag(&self) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScrollHandleOffsetable for ScrollHandle {
|
impl ScrollHandleOffsetable for ScrollHandle {
|
||||||
|
|
@ -82,16 +82,34 @@ impl ScrollHandleOffsetable for UniformListScrollHandle {
|
||||||
self.0.borrow_mut().base_handle.set_offset(offset)
|
self.0.borrow_mut().base_handle.set_offset(offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_uniform_list(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn content_size(&self) -> Size<Pixels> {
|
fn content_size(&self) -> Size<Pixels> {
|
||||||
let base_handle = &self.0.borrow().base_handle;
|
let base_handle = &self.0.borrow().base_handle;
|
||||||
base_handle.max_offset() + base_handle.bounds().size
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct ScrollbarState(Rc<Cell<ScrollbarStateInner>>);
|
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.
|
/// Create with vertical and horizontal scrollbar.
|
||||||
pub fn both(
|
pub fn both(
|
||||||
state: &ScrollbarState,
|
state: &ScrollbarState,
|
||||||
|
|
@ -778,6 +801,7 @@ impl Element for Scrollbar {
|
||||||
// click on the thumb bar, set the drag position
|
// click on the thumb bar, set the drag position
|
||||||
let pos = event.position - thumb_bounds.origin;
|
let pos = event.position - thumb_bounds.origin;
|
||||||
|
|
||||||
|
scroll_handle.start_drag();
|
||||||
state.set(state.get().with_drag_pos(axis, pos));
|
state.set(state.get().with_drag_pos(axis, pos));
|
||||||
|
|
||||||
cx.notify(view_id);
|
cx.notify(view_id);
|
||||||
|
|
@ -854,6 +878,9 @@ impl Element for Scrollbar {
|
||||||
|
|
||||||
// Move thumb position on dragging
|
// Move thumb position on dragging
|
||||||
if state.get().dragged_axis == Some(axis) && event.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
|
// drag_pos is the position of the mouse down event
|
||||||
// We need to keep the thumb bar still at the origin down position
|
// We need to keep the thumb bar still at the origin down position
|
||||||
let drag_pos = state.get().drag_pos;
|
let drag_pos = state.get().drag_pos;
|
||||||
|
|
@ -900,10 +927,12 @@ impl Element for Scrollbar {
|
||||||
});
|
});
|
||||||
|
|
||||||
window.on_mouse_event({
|
window.on_mouse_event({
|
||||||
|
let scroll_handle = self.scroll_handle.clone();
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
|
|
||||||
move |_event: &MouseUpEvent, phase, _, cx| {
|
move |_event: &MouseUpEvent, phase, _, cx| {
|
||||||
if phase.bubble() {
|
if phase.bubble() {
|
||||||
|
scroll_handle.end_drag();
|
||||||
state.set(state.get().with_unset_drag_pos());
|
state.set(state.get().with_unset_drag_pos());
|
||||||
cx.notify(view_id);
|
cx.notify(view_id);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use crate::{
|
||||||
context_menu::ContextMenuExt,
|
context_menu::ContextMenuExt,
|
||||||
h_flex,
|
h_flex,
|
||||||
popup_menu::PopupMenu,
|
popup_menu::PopupMenu,
|
||||||
scroll::{self, ScrollableMask, Scrollbar, ScrollbarState},
|
scroll::{ScrollableMask, Scrollbar, ScrollbarState},
|
||||||
v_flex, ActiveTheme, Icon, IconName, Sizable, Size, StyleSized as _, StyledExt,
|
v_flex, ActiveTheme, Icon, IconName, Sizable, Size, StyleSized as _, StyledExt,
|
||||||
VirtualListScrollHandle,
|
VirtualListScrollHandle,
|
||||||
};
|
};
|
||||||
|
|
@ -688,7 +688,7 @@ where
|
||||||
.top(self.size.table_row_height())
|
.top(self.size.table_row_height())
|
||||||
.right_0()
|
.right_0()
|
||||||
.bottom_0()
|
.bottom_0()
|
||||||
.w(scroll::WIDTH)
|
.w(Scrollbar::width())
|
||||||
.on_scroll_wheel(cx.listener(|_, _: &ScrollWheelEvent, _, cx| {
|
.on_scroll_wheel(cx.listener(|_, _: &ScrollWheelEvent, _, cx| {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}))
|
}))
|
||||||
|
|
@ -709,7 +709,7 @@ where
|
||||||
.left(self.fixed_head_cols_bounds.size.width)
|
.left(self.fixed_head_cols_bounds.size.width)
|
||||||
.right_0()
|
.right_0()
|
||||||
.bottom_0()
|
.bottom_0()
|
||||||
.h(scroll::WIDTH)
|
.h(Scrollbar::width())
|
||||||
.on_scroll_wheel(cx.listener(|_, _: &ScrollWheelEvent, _, cx| {
|
.on_scroll_wheel(cx.listener(|_, _: &ScrollWheelEvent, _, cx| {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ use std::{
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, img, prelude::FluentBuilder as _, px, relative, rems, AnyElement, App, DefiniteLength,
|
div, img, prelude::FluentBuilder as _, px, relative, rems, AnyElement, App, DefiniteLength,
|
||||||
Div, ElementId, FontStyle, FontWeight, Half, HighlightStyle, InteractiveElement as _,
|
Div, Element, ElementId, FontStyle, FontWeight, Half, HighlightStyle, InteractiveElement as _,
|
||||||
IntoElement, Length, ObjectFit, ParentElement, SharedString, SharedUri,
|
IntoElement, Length, ListState, ObjectFit, ParentElement, SharedString, SharedUri,
|
||||||
StatefulInteractiveElement, Styled, StyledImage as _, Window,
|
StatefulInteractiveElement, Styled, StyledImage as _, Window,
|
||||||
};
|
};
|
||||||
use markdown::mdast;
|
use markdown::mdast;
|
||||||
|
|
@ -111,7 +111,7 @@ impl PartialEq for ImageNode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Clone, Debug)]
|
||||||
pub(crate) struct InlineNode {
|
pub(crate) struct InlineNode {
|
||||||
/// The text content.
|
/// The text content.
|
||||||
pub(crate) text: SharedString,
|
pub(crate) text: SharedString,
|
||||||
|
|
@ -154,7 +154,7 @@ impl InlineNode {
|
||||||
///
|
///
|
||||||
/// Unlike other Element, this is cloneable, because it is used in the Node AST.
|
/// Unlike other Element, this is cloneable, because it is used in the Node AST.
|
||||||
/// We are keep the selection state inside this AST Nodes.
|
/// We are keep the selection state inside this AST Nodes.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub(crate) struct Paragraph {
|
pub(crate) struct Paragraph {
|
||||||
pub(super) span: Option<Span>,
|
pub(super) span: Option<Span>,
|
||||||
pub(super) children: Vec<InlineNode>,
|
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(crate) struct Table {
|
||||||
pub children: Vec<TableRow>,
|
pub children: Vec<TableRow>,
|
||||||
pub column_aligns: Vec<ColumnumnAlign>,
|
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(crate) struct TableRow {
|
||||||
pub children: Vec<TableCell>,
|
pub children: Vec<TableCell>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, PartialEq)]
|
#[derive(Debug, Clone, Default, PartialEq)]
|
||||||
pub(crate) struct TableCell {
|
pub(crate) struct TableCell {
|
||||||
pub children: Paragraph,
|
pub children: Paragraph,
|
||||||
pub width: Option<DefiniteLength>,
|
pub width: Option<DefiniteLength>,
|
||||||
|
|
@ -354,25 +354,34 @@ impl CodeBlock {
|
||||||
text
|
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;
|
let style = &node_cx.style;
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id("codeblock")
|
.when(!options.is_last, |this| this.pb(style.paragraph_gap))
|
||||||
.mb(style.paragraph_gap)
|
.child(
|
||||||
.p_3()
|
div()
|
||||||
.rounded(cx.theme().radius)
|
.id("codeblock")
|
||||||
.bg(cx.theme().secondary.opacity(0.85))
|
.p_3()
|
||||||
.font_family("Menlo, Monaco, Consolas, monospace")
|
.rounded(cx.theme().radius)
|
||||||
.text_size(rems(0.875))
|
.bg(cx.theme().secondary.opacity(0.85))
|
||||||
.relative()
|
.font_family("Menlo, Monaco, Consolas, monospace")
|
||||||
.refine_style(&style.code_block)
|
.text_size(rems(0.875))
|
||||||
.child(Inline::new(
|
.relative()
|
||||||
"code",
|
.refine_style(&style.code_block)
|
||||||
self.state.clone(),
|
.child(Inline::new(
|
||||||
vec![],
|
"code",
|
||||||
self.styles.clone(),
|
self.state.clone(),
|
||||||
))
|
vec![],
|
||||||
|
self.styles.clone(),
|
||||||
|
)),
|
||||||
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -391,7 +400,7 @@ impl NodeContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The AST Node of the rich text.
|
/// The AST Node of the rich text.
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub(crate) enum Node {
|
pub(crate) enum Node {
|
||||||
Root {
|
Root {
|
||||||
children: Vec<Node>,
|
children: Vec<Node>,
|
||||||
|
|
@ -651,337 +660,19 @@ impl Paragraph {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default, Clone, Copy)]
|
||||||
pub(crate) struct ListState {
|
struct NodeRenderOptions {
|
||||||
|
in_list: bool,
|
||||||
todo: bool,
|
todo: bool,
|
||||||
ordered: bool,
|
ordered: bool,
|
||||||
depth: usize,
|
depth: usize,
|
||||||
|
is_last: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Node {
|
impl NodeRenderOptions {
|
||||||
fn render_list_item(
|
fn is_last(mut self, is_last: bool) -> Self {
|
||||||
item: &Node,
|
self.is_last = is_last;
|
||||||
ix: usize,
|
self
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1166,3 +857,374 @@ impl Node {
|
||||||
.to_string()
|
.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::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, AnyElement, App, AppContext, Bounds, ClipboardItem, Context, Element, ElementId, Entity,
|
div, px, AnyElement, App, AppContext, Bounds, ClipboardItem, Context, Element, ElementId,
|
||||||
EntityId, FocusHandle, GlobalElementId, InspectorElementId, InteractiveElement, IntoElement,
|
Entity, EntityId, FocusHandle, GlobalElementId, InspectorElementId, InteractiveElement,
|
||||||
KeyBinding, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels,
|
IntoElement, KeyBinding, LayoutId, ListState, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
||||||
Point, RenderOnce, SharedString, Size, Styled, Timer, Window,
|
ParentElement, Pixels, Point, RenderOnce, SharedString, Size, StyleRefinement, Styled, Timer,
|
||||||
|
Window,
|
||||||
};
|
};
|
||||||
use smol::stream::StreamExt;
|
use smol::stream::StreamExt;
|
||||||
|
|
||||||
use crate::highlighter::HighlightTheme;
|
use crate::highlighter::HighlightTheme;
|
||||||
|
use crate::scroll::{Scrollbar, ScrollbarState};
|
||||||
use crate::{
|
use crate::{
|
||||||
global_state::GlobalState,
|
global_state::GlobalState,
|
||||||
input::{self},
|
input::{self},
|
||||||
|
|
@ -22,7 +24,7 @@ use crate::{
|
||||||
TextViewStyle,
|
TextViewStyle,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use crate::{v_flex, ActiveTheme};
|
use crate::{v_flex, ActiveTheme, StyledExt};
|
||||||
|
|
||||||
const CONTEXT: &'static str = "TextView";
|
const CONTEXT: &'static str = "TextView";
|
||||||
|
|
||||||
|
|
@ -37,29 +39,30 @@ pub(crate) fn init(cx: &mut App) {
|
||||||
|
|
||||||
#[derive(IntoElement, Clone)]
|
#[derive(IntoElement, Clone)]
|
||||||
struct TextViewElement {
|
struct TextViewElement {
|
||||||
|
list_state: Option<ListState>,
|
||||||
state: Entity<TextViewState>,
|
state: Entity<TextViewState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for TextViewElement {
|
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 {
|
v_flex()
|
||||||
Some(Ok(content)) => this.child(content.root_node.render(
|
.size_full()
|
||||||
None,
|
.map(|this| match &mut state.parsed_result {
|
||||||
true,
|
Some(Ok(content)) => this.child(content.root_node.render_root(
|
||||||
true,
|
self.list_state.clone(),
|
||||||
&content.node_cx,
|
&content.node_cx,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)),
|
)),
|
||||||
Some(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,
|
None => this,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -85,7 +88,9 @@ pub struct TextView {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
init_state: Option<InitState>,
|
init_state: Option<InitState>,
|
||||||
state: Entity<TextViewState>,
|
state: Entity<TextViewState>,
|
||||||
|
style: StyleRefinement,
|
||||||
selectable: bool,
|
selectable: bool,
|
||||||
|
scrollable: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
|
|
@ -205,7 +210,6 @@ pub(crate) struct TextViewState {
|
||||||
tx: Option<smol::channel::Sender<Update>>,
|
tx: Option<smol::channel::Sender<Update>>,
|
||||||
parsed_result: Option<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
|
||||||
bounds: Bounds<Pixels>,
|
bounds: Bounds<Pixels>,
|
||||||
/// The local (in TextView) position of the selection.
|
/// The local (in TextView) position of the selection.
|
||||||
|
|
@ -213,6 +217,8 @@ pub(crate) struct TextViewState {
|
||||||
/// Is current in selection.
|
/// Is current in selection.
|
||||||
is_selecting: bool,
|
is_selecting: bool,
|
||||||
is_selectable: bool,
|
is_selectable: bool,
|
||||||
|
scrollbar_state: ScrollbarState,
|
||||||
|
list_state: ListState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextViewState {
|
impl TextViewState {
|
||||||
|
|
@ -227,6 +233,8 @@ impl TextViewState {
|
||||||
selection_positions: (None, None),
|
selection_positions: (None, None),
|
||||||
is_selecting: false,
|
is_selecting: false,
|
||||||
is_selectable: 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 {
|
impl TextView {
|
||||||
fn create_init_state(
|
fn create_init_state(
|
||||||
type_: TextViewType,
|
type_: TextViewType,
|
||||||
|
|
@ -394,8 +408,10 @@ impl TextView {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
init_state: Some(init_state),
|
init_state: Some(init_state),
|
||||||
|
style: StyleRefinement::default(),
|
||||||
state,
|
state,
|
||||||
selectable: false,
|
selectable: false,
|
||||||
|
scrollable: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -421,8 +437,10 @@ impl TextView {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
init_state: Some(init_state),
|
init_state: Some(init_state),
|
||||||
|
style: StyleRefinement::default(),
|
||||||
state,
|
state,
|
||||||
selectable: false,
|
selectable: false,
|
||||||
|
scrollable: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -459,6 +477,23 @@ impl TextView {
|
||||||
self
|
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) {
|
fn on_action_copy(state: &Entity<TextViewState>, cx: &mut App) {
|
||||||
let Some(selected_text) = state.read(cx).selection_text() else {
|
let Some(selected_text) = state.read(cx).selection_text() else {
|
||||||
return;
|
return;
|
||||||
|
|
@ -553,6 +588,9 @@ impl Element for TextView {
|
||||||
self.init_state = Some(InitState::Initialized { tx });
|
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
|
let focus_handle = self
|
||||||
.state
|
.state
|
||||||
.read(cx)
|
.read(cx)
|
||||||
|
|
@ -563,6 +601,8 @@ impl Element for TextView {
|
||||||
let mut el = div()
|
let mut el = div()
|
||||||
.key_context(CONTEXT)
|
.key_context(CONTEXT)
|
||||||
.track_focus(focus_handle)
|
.track_focus(focus_handle)
|
||||||
|
.size_full()
|
||||||
|
.relative()
|
||||||
.on_action({
|
.on_action({
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
move |_: &input::Copy, _, cx| {
|
move |_: &input::Copy, _, cx| {
|
||||||
|
|
@ -570,8 +610,25 @@ impl Element for TextView {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.child(TextViewElement {
|
.child(TextViewElement {
|
||||||
|
list_state: if self.scrollable {
|
||||||
|
Some(list_state.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
state: self.state.clone(),
|
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();
|
.into_any_element();
|
||||||
let layout_id = el.request_layout(window, cx);
|
let layout_id = el.request_layout(window, cx);
|
||||||
(layout_id, el)
|
(layout_id, el)
|
||||||
|
|
@ -636,7 +693,11 @@ impl Element for TextView {
|
||||||
// move to update end position.
|
// move to update end position.
|
||||||
window.on_mouse_event({
|
window.on_mouse_event({
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
move |event: &MouseMoveEvent, _, _, cx| {
|
move |event: &MouseMoveEvent, phase, _, cx| {
|
||||||
|
if !phase.bubble() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
state.update(cx, |state, _| {
|
state.update(cx, |state, _| {
|
||||||
state.update_selection(event.position);
|
state.update_selection(event.position);
|
||||||
});
|
});
|
||||||
|
|
@ -647,7 +708,11 @@ impl Element for TextView {
|
||||||
// up to end selection
|
// up to end selection
|
||||||
window.on_mouse_event({
|
window.on_mouse_event({
|
||||||
let state = self.state.clone();
|
let state = self.state.clone();
|
||||||
move |_: &MouseUpEvent, _, _, cx| {
|
move |_: &MouseUpEvent, phase, _, cx| {
|
||||||
|
if !phase.bubble() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
state.update(cx, |state, _| {
|
state.update(cx, |state, _| {
|
||||||
state.end_selection();
|
state.end_selection();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue