feat: add markdown text view
This commit is contained in:
+188
-129
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user