feat: add markdown text view

This commit is contained in:
2026-11-10 18:14:10 +07:00
parent 7c061edfba
commit a6b6cfd5a4
5 changed files with 1660 additions and 261 deletions
+188 -129
View File
@@ -46,7 +46,7 @@ use gpui_native_data::tree::{Tree, TreeItem};
use gpui_native_editor::code_view::CodeViewState;
use gpui_native_editor::diff_view::{DiffRowKind, DiffViewState};
use gpui_native_editor::highlight::Language;
use gpui_native_editor::markdown::{MarkdownBlock, MarkdownViewState};
use gpui_native_editor::markdown::{MarkdownBlock, MarkdownViewState, TextView};
use gpui_native_icons::{IconAssets, IconRole, icon};
use gpui_native_layout::dock::{Dock, DockSidebar, DockState, PanelDescriptor, SplitDirection};
use gpui_native_layout::resizable::{Resizable, ResizableState};
@@ -98,6 +98,7 @@ const CATALOG: &[(&str, &[(&str, &str)])] = &[
("code-view", "Code view"),
("diff-view", "Diff view"),
("markdown-view", "Markdown view"),
("text-view", "Text view"),
],
),
(
@@ -612,6 +613,9 @@ impl Gallery {
"Markdown view",
|_, cx| cx.new(MarkdownViewDemo::new).into(),
));
dock.register_panel(PanelDescriptor::new("text-view", "Text view", |_, cx| {
cx.new(TextViewDemo::new).into()
}));
dock.register_panel(PanelDescriptor::new("sidebar", "Sidebar", |_, cx| {
cx.new(|_| SidebarDemo { selected: None }).into()
}));
@@ -2191,30 +2195,28 @@ impl Render for TreeDemo {
let mut paths = Vec::new();
Self::flatten(&self.nodes, 0, &[], "", &mut items, &mut paths);
ScrollArea::new("tree-demo")
.size_full()
.child(demo_section(
"outline",
colors,
Tree::new("tree-demo-tree")
.selected(self.selected)
.on_change(move |selected, _, cx| {
view.update(cx, |demo, cx| {
demo.selected = selected;
cx.notify();
});
})
.on_disclosure(move |index, _, cx| {
view_disclosure.update(cx, |demo, cx| {
let paths = TreeDemo::paths(&demo.nodes);
if let Some(path) = paths.get(index) {
TreeDemo::toggle(&mut demo.nodes, path);
}
cx.notify();
});
})
.items(move |_, _| items.clone()),
))
ScrollArea::new("tree-demo").size_full().child(demo_section(
"outline",
colors,
Tree::new("tree-demo-tree")
.selected(self.selected)
.on_change(move |selected, _, cx| {
view.update(cx, |demo, cx| {
demo.selected = selected;
cx.notify();
});
})
.on_disclosure(move |index, _, cx| {
view_disclosure.update(cx, |demo, cx| {
let paths = TreeDemo::paths(&demo.nodes);
if let Some(path) = paths.get(index) {
TreeDemo::toggle(&mut demo.nodes, path);
}
cx.notify();
});
})
.items(move |_, _| items.clone()),
))
}
}
@@ -2645,42 +2647,40 @@ impl Render for MenuDemo {
let view = cx.entity();
let colors = cx.theme().colors;
ScrollArea::new("menu-demo")
.size_full()
.child(demo_section(
"trigger and items",
colors,
Menu::new("menu-demo-menu")
.open(self.open)
.on_change(move |open, _, cx| {
view.update(cx, |demo, cx| {
demo.open = open;
cx.notify();
});
})
.trigger(|_, _| {
Button::new("menu-demo-trigger")
.label("Actions")
.variant(ButtonVariant::Secondary)
.into_any_element()
})
.items(|_, _| {
vec![
MenuItem::new("Cut").shortcut("cmd-x").handler(|_, cx| {
eprintln!("menu: cut");
let _ = cx;
}),
MenuItem::new("Copy").shortcut("cmd-c"),
MenuItem::new("Paste").shortcut("cmd-v"),
MenuItem::separator(),
MenuItem::new("Use quiet mode").checked(true),
MenuItem::new("Show hidden files"),
MenuItem::separator(),
MenuItem::new("Unavailable").disabled(true),
MenuItem::new("Delete").destructive(true),
]
}),
))
ScrollArea::new("menu-demo").size_full().child(demo_section(
"trigger and items",
colors,
Menu::new("menu-demo-menu")
.open(self.open)
.on_change(move |open, _, cx| {
view.update(cx, |demo, cx| {
demo.open = open;
cx.notify();
});
})
.trigger(|_, _| {
Button::new("menu-demo-trigger")
.label("Actions")
.variant(ButtonVariant::Secondary)
.into_any_element()
})
.items(|_, _| {
vec![
MenuItem::new("Cut").shortcut("cmd-x").handler(|_, cx| {
eprintln!("menu: cut");
let _ = cx;
}),
MenuItem::new("Copy").shortcut("cmd-c"),
MenuItem::new("Paste").shortcut("cmd-v"),
MenuItem::separator(),
MenuItem::new("Use quiet mode").checked(true),
MenuItem::new("Show hidden files"),
MenuItem::separator(),
MenuItem::new("Unavailable").disabled(true),
MenuItem::new("Delete").destructive(true),
]
}),
))
}
}
@@ -2835,30 +2835,28 @@ impl Render for DropdownMenuDemo {
]
});
ScrollArea::new("dropdown-menu-demo")
.size_full()
.child(
v_flex()
.gap_3()
.child(demo_section(
"menu-only trigger and split button",
colors,
h_flex().gap_3().child(export).child(save),
))
.child(demo_section(
"trigger variants",
colors,
h_flex().gap_3().child(duplicate).child(delete).child(more),
))
.child(demo_section(
"status",
colors,
div()
.text_sm()
.text_color(colors.muted_foreground)
.child(status),
)),
)
ScrollArea::new("dropdown-menu-demo").size_full().child(
v_flex()
.gap_3()
.child(demo_section(
"menu-only trigger and split button",
colors,
h_flex().gap_3().child(export).child(save),
))
.child(demo_section(
"trigger variants",
colors,
h_flex().gap_3().child(duplicate).child(delete).child(more),
))
.child(demo_section(
"status",
colors,
div()
.text_sm()
.text_color(colors.muted_foreground)
.child(status),
)),
)
}
}
@@ -2871,51 +2869,49 @@ impl Render for ContextMenuDemo {
let view = cx.entity();
let colors = cx.theme().colors;
ScrollArea::new("context-menu-demo")
.size_full()
.child(
v_flex()
.gap_3()
.child(demo_section(
"on any target",
colors,
ContextMenu::new("context-menu-demo-menu")
.target(move |_, cx| {
let theme = cx.theme();
div()
.px_3()
.py_2()
.rounded(theme.radius.overlay)
.border_1()
.border_color(theme.colors.border)
.bg(theme.colors.surface)
.text_size(theme.sizes.medium.text_size)
.child("Right-click this card")
.into_any_element()
})
.items(move |_, _| {
CONTEXT_ACTIONS
.iter()
.map(|action| {
let view = view.clone();
MenuItem::new(*action).handler(move |_, cx| {
view.update(cx, |demo, cx| {
demo.action = SharedString::from(*action);
cx.notify();
});
})
ScrollArea::new("context-menu-demo").size_full().child(
v_flex()
.gap_3()
.child(demo_section(
"on any target",
colors,
ContextMenu::new("context-menu-demo-menu")
.target(move |_, cx| {
let theme = cx.theme();
div()
.px_3()
.py_2()
.rounded(theme.radius.overlay)
.border_1()
.border_color(theme.colors.border)
.bg(theme.colors.surface)
.text_size(theme.sizes.medium.text_size)
.child("Right-click this card")
.into_any_element()
})
.items(move |_, _| {
CONTEXT_ACTIONS
.iter()
.map(|action| {
let view = view.clone();
MenuItem::new(*action).handler(move |_, cx| {
view.update(cx, |demo, cx| {
demo.action = SharedString::from(*action);
cx.notify();
});
})
.collect()
})
.accessibility_label("Card actions"),
))
.child(
div()
.text_sm()
.text_color(colors.muted_foreground)
.child(self.action.clone()),
),
)
})
.collect()
})
.accessibility_label("Card actions"),
))
.child(
div()
.text_sm()
.text_color(colors.muted_foreground)
.child(self.action.clone()),
),
)
}
}
@@ -3734,6 +3730,68 @@ impl Render for MarkdownViewDemo {
}
}
/// The text view demo's sample document.
const TEXT_VIEW_SAMPLE: &str = r###"Try the [relay docs](https://example.com/relay) or jump to [issue #71](https://example.com/issues/71).
Links route through the handler below — nothing opens on its own.
"###;
/// The text view demo: an embeddable markdown view whose links the app routes.
struct TextViewDemo {
clicked_links: Vec<String>,
}
impl TextViewDemo {
fn new(_cx: &mut Context<Self>) -> Self {
Self {
clicked_links: Vec::new(),
}
}
}
impl Render for TextViewDemo {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let colors = cx.theme().colors;
let radius = cx.theme().radius;
let clicks = cx.entity().downgrade();
let last_clicked = self.clicked_links.last().cloned();
ScrollArea::new("text-view-demo").size_full().child(
v_flex().gap_3().child(demo_section(
"embedded, fit-content; links route through the app",
colors,
v_flex()
.gap_2()
.child(
div()
.w_full()
.rounded(radius.control)
.border_1()
.border_color(colors.border)
.p_3()
.child(
TextView::markdown("text-view-demo", TEXT_VIEW_SAMPLE)
.on_link_click(move |url, _window, cx| {
clicks
.update(cx, |demo, cx| {
demo.clicked_links.push(url.to_string());
cx.notify();
})
.log_err();
}),
),
)
.child(div().text_xs().text_color(colors.muted_foreground).child(
match last_clicked {
Some(url) => format!("last routed link: {url}"),
None => "click a link to route it".to_string(),
},
)),
)),
)
}
}
struct SidebarDemo {
selected: Option<usize>,
}
@@ -4053,6 +4111,7 @@ fn main() {
.run(|cx: &mut App| {
gpui_native_core::init(cx);
gpui_native_editor::code_view::bind_code_view_keys(cx);
gpui_native_editor::markdown::bind_text_view_keys(cx);
cx.bind_keys([
KeyBinding::new("cmd-k", OpenCommandPalette, None),