Add to support dump/load Dock layout. (#226)
This commit is contained in:
parent
2f80ba408a
commit
bf134b2ff9
12 changed files with 529 additions and 117 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
|||
/target
|
||||
.DS_Store
|
||||
layout.json
|
||||
|
|
|
|||
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -2294,6 +2294,7 @@ dependencies = [
|
|||
"log",
|
||||
"rust-embed",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"story",
|
||||
"ui",
|
||||
"workspace",
|
||||
|
|
@ -4854,6 +4855,7 @@ dependencies = [
|
|||
"gpui",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"ui",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ workspace.workspace = true
|
|||
ui.workspace = true
|
||||
story.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
use anyhow::Result;
|
||||
use gpui::*;
|
||||
use prelude::FluentBuilder as _;
|
||||
use private::serde::Deserialize;
|
||||
use std::sync::Arc;
|
||||
use story::{
|
||||
ButtonStory, CalendarStory, DropdownStory, IconStory, ImageStory, InputStory, ListStory,
|
||||
ModalStory, PopupStory, ProgressStory, ResizableStory, ScrollableStory, StoryContainer,
|
||||
SwitchStory, TableStory, TextStory, TooltipStory,
|
||||
};
|
||||
use workspace::TitleBar;
|
||||
|
||||
use std::sync::Arc;
|
||||
use ui::{
|
||||
button::Button,
|
||||
color_picker::{ColorPicker, ColorPickerEvent},
|
||||
dock::{DockArea, DockItem},
|
||||
dock::{DockArea, DockEvent, DockItem, DockItemState},
|
||||
drawer::Drawer,
|
||||
h_flex,
|
||||
modal::Modal,
|
||||
|
|
@ -20,6 +19,7 @@ use ui::{
|
|||
theme::{ActiveTheme, Colorize as _, Theme},
|
||||
ContextModal, IconName, Root, Sizable,
|
||||
};
|
||||
use workspace::TitleBar;
|
||||
|
||||
use crate::app_state::AppState;
|
||||
|
||||
|
|
@ -52,8 +52,73 @@ impl StoryWorkspace {
|
|||
.detach();
|
||||
|
||||
let dock_area = cx.new_view(|cx| DockArea::new("main-dock", cx));
|
||||
let weak_dock_area = dock_area.downgrade();
|
||||
|
||||
let dock_item = DockItem::split_with_sizes(
|
||||
let dock_item = match Self::load_layout(&weak_dock_area, cx) {
|
||||
Ok(item) => item,
|
||||
Err(err) => {
|
||||
eprintln!("load layout error: {:?}", err);
|
||||
Self::init_default_layout(&weak_dock_area, cx)
|
||||
}
|
||||
};
|
||||
|
||||
dock_area.update(cx, |view, cx| view.set_root(dock_item, cx));
|
||||
|
||||
cx.subscribe(&dock_area, |_, dock_area, ev: &DockEvent, cx| match ev {
|
||||
DockEvent::LayoutChanged => {
|
||||
// Make debounce
|
||||
|
||||
println!("Saving layout...");
|
||||
let json = dock_area.read(cx).dump(cx).unwrap();
|
||||
// Save layout json to app dir layout.json
|
||||
std::fs::write("layout.json", json).unwrap();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let locale_selector = cx.new_view(LocaleSelector::new);
|
||||
|
||||
let theme_color_picker = cx.new_view(|cx| {
|
||||
let mut picker = ColorPicker::new("theme-color-picker", cx)
|
||||
.xsmall()
|
||||
.anchor(AnchorCorner::TopRight)
|
||||
.label("Primary Color");
|
||||
picker.set_value(cx.theme().primary, cx);
|
||||
picker
|
||||
});
|
||||
cx.subscribe(
|
||||
&theme_color_picker,
|
||||
|_, _, ev: &ColorPickerEvent, cx| match ev {
|
||||
ColorPickerEvent::Change(color) => {
|
||||
if let Some(color) = color {
|
||||
let theme = cx.global_mut::<Theme>();
|
||||
theme.primary = *color;
|
||||
theme.primary_hover = color.lighten(0.1);
|
||||
theme.primary_active = color.darken(0.1);
|
||||
cx.refresh();
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
dock_area,
|
||||
locale_selector,
|
||||
theme_color_picker,
|
||||
}
|
||||
}
|
||||
|
||||
fn load_layout(dock_area: &WeakView<DockArea>, cx: &mut WindowContext) -> Result<DockItem> {
|
||||
let fname = "layout.json";
|
||||
let json = std::fs::read_to_string(fname)?;
|
||||
let state = serde_json::from_str::<DockItemState>(&json)?;
|
||||
|
||||
return Ok(state.to_item(dock_area.clone(), cx));
|
||||
}
|
||||
|
||||
fn init_default_layout(dock_area: &WeakView<DockArea>, cx: &mut WindowContext) -> DockItem {
|
||||
DockItem::split_with_sizes(
|
||||
Axis::Horizontal,
|
||||
vec![
|
||||
DockItem::split(
|
||||
|
|
@ -116,41 +181,7 @@ impl StoryWorkspace {
|
|||
vec![Some(px(300.)), None, Some(px(350.))],
|
||||
&dock_area,
|
||||
cx,
|
||||
);
|
||||
|
||||
dock_area.update(cx, |view, cx| view.set_root(dock_item, cx));
|
||||
|
||||
let locale_selector = cx.new_view(LocaleSelector::new);
|
||||
|
||||
let theme_color_picker = cx.new_view(|cx| {
|
||||
let mut picker = ColorPicker::new("theme-color-picker", cx)
|
||||
.xsmall()
|
||||
.anchor(AnchorCorner::TopRight)
|
||||
.label("Primary Color");
|
||||
picker.set_value(cx.theme().primary, cx);
|
||||
picker
|
||||
});
|
||||
cx.subscribe(
|
||||
&theme_color_picker,
|
||||
|_, _, ev: &ColorPickerEvent, cx| match ev {
|
||||
ColorPickerEvent::Change(color) => {
|
||||
if let Some(color) = color {
|
||||
let theme = cx.global_mut::<Theme>();
|
||||
theme.primary = *color;
|
||||
theme.primary_hover = color.lighten(0.1);
|
||||
theme.primary_active = color.darken(0.1);
|
||||
cx.refresh();
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
dock_area,
|
||||
locale_selector,
|
||||
theme_color_picker,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_local(
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ charts-rs = "0.3"
|
|||
regex = "1"
|
||||
chrono = "0.4"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ pub use popup_story::PopupStory;
|
|||
pub use progress_story::ProgressStory;
|
||||
pub use resizable_story::ResizableStory;
|
||||
pub use scrollable_story::ScrollableStory;
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use switch_story::SwitchStory;
|
||||
pub use table_story::TableStory;
|
||||
pub use text_story::TextStory;
|
||||
|
|
@ -42,7 +43,7 @@ use gpui::{
|
|||
|
||||
use ui::{
|
||||
divider::Divider,
|
||||
dock::{Panel, PanelEvent, TitleStyle},
|
||||
dock::{register_panel, DockItemInfo, DockItemState, Panel, PanelEvent, TitleStyle},
|
||||
h_flex,
|
||||
label::Label,
|
||||
notification::Notification,
|
||||
|
|
@ -55,6 +56,24 @@ pub fn init(cx: &mut AppContext) {
|
|||
input_story::init(cx);
|
||||
dropdown_story::init(cx);
|
||||
popup_story::init(cx);
|
||||
|
||||
register_panel(cx, "StoryContainer", |_, info, cx| {
|
||||
let story_state = match info {
|
||||
DockItemInfo::Custom(value) => StoryState::from_value(value),
|
||||
_ => {
|
||||
unreachable!("Invalid DockItemInfo: {:?}", info)
|
||||
}
|
||||
};
|
||||
|
||||
let view = cx.new_view(|cx| {
|
||||
let (title, description, story) = story_state.to_story(cx);
|
||||
let mut container = StoryContainer::new(cx).story(story, story_state.story_klass);
|
||||
container.name = title.into();
|
||||
container.description = description.into();
|
||||
container
|
||||
});
|
||||
Box::new(view)
|
||||
});
|
||||
}
|
||||
|
||||
actions!(story, [PanelInfo]);
|
||||
|
|
@ -95,7 +114,7 @@ pub enum ContainerEvent {
|
|||
|
||||
pub trait Story {
|
||||
fn klass() -> &'static str {
|
||||
std::any::type_name::<Self>()
|
||||
std::any::type_name::<Self>().split("::").last().unwrap()
|
||||
}
|
||||
|
||||
fn title() -> &'static str;
|
||||
|
|
@ -114,7 +133,7 @@ pub trait Story {
|
|||
impl EventEmitter<ContainerEvent> for StoryContainer {}
|
||||
|
||||
impl StoryContainer {
|
||||
pub fn new(closeable: bool, cx: &mut WindowContext) -> Self {
|
||||
pub fn new(cx: &mut WindowContext) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
Self {
|
||||
|
|
@ -126,7 +145,7 @@ impl StoryContainer {
|
|||
height: None,
|
||||
story: None,
|
||||
story_klass: None,
|
||||
closeable,
|
||||
closeable: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -138,7 +157,8 @@ impl StoryContainer {
|
|||
let story_klass = S::klass();
|
||||
|
||||
let view = cx.new_view(|cx| {
|
||||
let mut story = Self::new(S::closeable(), cx).story(story, story_klass);
|
||||
let mut story = Self::new(cx).story(story, story_klass);
|
||||
story.closeable = S::closeable();
|
||||
story.name = name.into();
|
||||
story.description = description.into();
|
||||
story.title_bg = S::title_bg();
|
||||
|
|
@ -172,7 +192,63 @@ impl StoryContainer {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct StoryState {
|
||||
pub story_klass: SharedString,
|
||||
}
|
||||
|
||||
impl StoryState {
|
||||
fn to_value(&self) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"story_klass": self.story_klass,
|
||||
})
|
||||
}
|
||||
|
||||
fn from_value(value: serde_json::Value) -> Self {
|
||||
serde_json::from_value(value).unwrap()
|
||||
}
|
||||
|
||||
fn to_story(&self, cx: &mut WindowContext) -> (&'static str, &'static str, AnyView) {
|
||||
macro_rules! story {
|
||||
($klass:tt) => {
|
||||
(
|
||||
$klass::title(),
|
||||
$klass::description(),
|
||||
$klass::view(cx).into(),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
match self.story_klass.to_string().as_str() {
|
||||
"ButtonStory" => story!(ButtonStory),
|
||||
"CalendarStory" => story!(CalendarStory),
|
||||
"DropdownStory" => story!(DropdownStory),
|
||||
"IconStory" => story!(IconStory),
|
||||
"ImageStory" => story!(ImageStory),
|
||||
"InputStory" => story!(InputStory),
|
||||
"ListStory" => story!(ListStory),
|
||||
"ModalStory" => story!(ModalStory),
|
||||
"PopupStory" => story!(PopupStory),
|
||||
"ProgressStory" => story!(ProgressStory),
|
||||
"ResizableStory" => story!(ResizableStory),
|
||||
"ScrollableStory" => story!(ScrollableStory),
|
||||
"SwitchStory" => story!(SwitchStory),
|
||||
"TableStory" => story!(TableStory),
|
||||
"TextStory" => story!(TextStory),
|
||||
"TooltipStory" => story!(TooltipStory),
|
||||
"WebViewStory" => story!(WebViewStory),
|
||||
_ => {
|
||||
unreachable!("Invalid story klass: {}", self.story_klass)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for StoryContainer {
|
||||
fn panel_name(&self) -> &'static str {
|
||||
"StoryContainer"
|
||||
}
|
||||
|
||||
fn title(&self, _cx: &WindowContext) -> SharedString {
|
||||
self.name.clone()
|
||||
}
|
||||
|
|
@ -196,6 +272,15 @@ impl Panel for StoryContainer {
|
|||
menu.track_focus(&self.focus_handle)
|
||||
.menu("Info", Box::new(PanelInfo))
|
||||
}
|
||||
|
||||
fn dump(&self, _cx: &AppContext) -> DockItemState {
|
||||
let mut state = DockItemState::new(self.panel_name());
|
||||
let story_state = StoryState {
|
||||
story_klass: self.story_klass.clone().unwrap(),
|
||||
};
|
||||
state.info = DockItemInfo::custom(story_state.to_value());
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for StoryContainer {}
|
||||
|
|
|
|||
|
|
@ -5,16 +5,29 @@ mod tab_panel;
|
|||
use std::sync::Arc;
|
||||
|
||||
use gpui::{
|
||||
actions, div, prelude::FluentBuilder, AnyElement, AnyView, Axis, InteractiveElement as _,
|
||||
IntoElement, ParentElement as _, Pixels, Render, SharedString, Styled, View, ViewContext,
|
||||
VisualContext, WindowContext,
|
||||
actions, div, prelude::FluentBuilder, AnyElement, AnyView, AppContext, Axis, EventEmitter,
|
||||
InteractiveElement as _, IntoElement, ParentElement as _, Pixels, Render, SharedString, Styled,
|
||||
View, ViewContext, VisualContext, WeakView, WindowContext,
|
||||
};
|
||||
pub use panel::*;
|
||||
pub use stack_panel::*;
|
||||
pub use tab_panel::*;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
stack_panel::init(cx);
|
||||
tab_panel::init(cx);
|
||||
}
|
||||
|
||||
actions!(dock, [ToggleZoom, ClosePanel]);
|
||||
|
||||
pub enum DockEvent {
|
||||
/// The layout of the dock has changed, subscribers this to save the layout.
|
||||
///
|
||||
/// This event is emitted when every time the layout of the dock has changed,
|
||||
/// So it emits may be too frequently, you may want to debounce the event.
|
||||
LayoutChanged,
|
||||
}
|
||||
|
||||
/// The main area of the dock.
|
||||
pub struct DockArea {
|
||||
id: SharedString,
|
||||
|
|
@ -43,7 +56,7 @@ impl DockItem {
|
|||
pub fn split(
|
||||
axis: Axis,
|
||||
items: Vec<DockItem>,
|
||||
dock_area: &View<DockArea>,
|
||||
dock_area: &WeakView<DockArea>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Self {
|
||||
let sizes = vec![None; items.len()];
|
||||
|
|
@ -58,7 +71,7 @@ impl DockItem {
|
|||
axis: Axis,
|
||||
items: Vec<DockItem>,
|
||||
sizes: Vec<Option<Pixels>>,
|
||||
dock_area: &View<DockArea>,
|
||||
dock_area: &WeakView<DockArea>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Self {
|
||||
let mut items = items;
|
||||
|
|
@ -67,13 +80,13 @@ impl DockItem {
|
|||
for (i, item) in items.iter_mut().enumerate() {
|
||||
let view = item.view();
|
||||
let size = sizes.get(i).copied().flatten();
|
||||
stack_panel.add_panel(view.clone(), size, dock_area.downgrade(), cx)
|
||||
stack_panel.add_panel(view.clone(), size, dock_area.clone(), cx)
|
||||
}
|
||||
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
let view = item.view();
|
||||
let size = sizes.get(i).copied().flatten();
|
||||
stack_panel.add_panel(view.clone(), size, dock_area.downgrade(), cx)
|
||||
stack_panel.add_panel(view.clone(), size, dock_area.clone(), cx)
|
||||
}
|
||||
|
||||
stack_panel
|
||||
|
|
@ -92,7 +105,7 @@ impl DockItem {
|
|||
pub fn tabs(
|
||||
items: Vec<Arc<dyn PanelView>>,
|
||||
active_ix: Option<usize>,
|
||||
dock_area: &View<DockArea>,
|
||||
dock_area: &WeakView<DockArea>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Self {
|
||||
let mut new_items: Vec<Arc<dyn PanelView>> = vec![];
|
||||
|
|
@ -104,7 +117,7 @@ impl DockItem {
|
|||
|
||||
pub fn tab<P: Panel>(
|
||||
item: View<P>,
|
||||
dock_area: &View<DockArea>,
|
||||
dock_area: &WeakView<DockArea>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Self {
|
||||
Self::new_tabs(vec![Arc::new(item.clone())], None, dock_area, cx)
|
||||
|
|
@ -113,16 +126,16 @@ impl DockItem {
|
|||
fn new_tabs(
|
||||
items: Vec<Arc<dyn PanelView>>,
|
||||
active_ix: Option<usize>,
|
||||
dock_area: &View<DockArea>,
|
||||
dock_area: &WeakView<DockArea>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Self {
|
||||
let active_ix = active_ix.unwrap_or(0);
|
||||
let tab_panel = cx.new_view(|cx| {
|
||||
let mut tab_panel = TabPanel::new(None, dock_area.downgrade(), cx);
|
||||
let mut tab_panel = TabPanel::new(None, dock_area.clone(), cx);
|
||||
for item in items.iter() {
|
||||
tab_panel.add_panel(item.clone(), cx)
|
||||
}
|
||||
|
||||
tab_panel.active_ix = active_ix;
|
||||
tab_panel
|
||||
});
|
||||
|
||||
|
|
@ -178,20 +191,21 @@ impl DockArea {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
/// Dump the dock panels layout to JSON string.
|
||||
pub fn dump(&self, cx: &AppContext) -> Result<String, serde_json::Error> {
|
||||
let root = self.items.view();
|
||||
let state = root.dump(cx);
|
||||
serde_json::to_string_pretty(&state)
|
||||
}
|
||||
|
||||
/// Subscribe event on the panels
|
||||
#[allow(clippy::only_used_in_recursion)]
|
||||
fn subscribe_item(&self, item: &DockItem, cx: &mut ViewContext<Self>) {
|
||||
let dock_area = cx.view();
|
||||
|
||||
/// Subscribe zoom event on the panel
|
||||
fn subscribe_zoom<P: Panel>(
|
||||
view: &View<P>,
|
||||
dock_area: View<DockArea>,
|
||||
cx: &mut ViewContext<DockArea>,
|
||||
) {
|
||||
fn subscribe_zoom<P: Panel>(view: &View<P>, cx: &mut ViewContext<DockArea>) {
|
||||
cx.subscribe(view, move |_, panel, event, cx| match event {
|
||||
PanelEvent::ZoomIn => {
|
||||
let dock_area = dock_area.clone();
|
||||
let dock_area = cx.view().clone();
|
||||
let panel = panel.clone();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let _ = cx.update(|cx| {
|
||||
|
|
@ -204,7 +218,7 @@ impl DockArea {
|
|||
.detach();
|
||||
}
|
||||
PanelEvent::ZoomOut => {
|
||||
let dock_area = dock_area.clone();
|
||||
let dock_area = cx.view().clone();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let _ = cx.update(|cx| {
|
||||
let _ = dock_area.update(cx, |view, cx| view.set_zoomed_out(cx));
|
||||
|
|
@ -212,20 +226,27 @@ impl DockArea {
|
|||
})
|
||||
.detach()
|
||||
}
|
||||
PanelEvent::LayoutChanged => cx.emit(DockEvent::LayoutChanged),
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
match item {
|
||||
DockItem::Split { items, .. } => {
|
||||
DockItem::Split { items, view, .. } => {
|
||||
for item in items {
|
||||
self.subscribe_item(item, cx);
|
||||
}
|
||||
|
||||
cx.subscribe(view, move |_, _, event, cx| match event {
|
||||
PanelEvent::LayoutChanged => cx.emit(DockEvent::LayoutChanged),
|
||||
_ => {}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
DockItem::Tabs { view, .. } => {
|
||||
// We need, only subscribe to the zoom events on the TabPanel
|
||||
// Because we always wrap the DockItem::Panel in a DockItem::Tabs
|
||||
subscribe_zoom(view, dock_area.clone(), cx);
|
||||
subscribe_zoom(view, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -252,7 +273,7 @@ impl DockArea {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DockEvent> for DockArea {}
|
||||
impl Render for DockArea {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
// println!("Rendering dock area");
|
||||
|
|
|
|||
|
|
@ -1,9 +1,21 @@
|
|||
use gpui::{AnyView, EventEmitter, FocusableView, Hsla, SharedString, View, WindowContext};
|
||||
use rust_i18n::t;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::popup_menu::PopupMenu;
|
||||
use gpui::{
|
||||
AnyView, AppContext, Axis, EventEmitter, FocusableView, Global, Hsla, Pixels, SharedString,
|
||||
View, WeakView, WindowContext,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use rust_i18n::t;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::PanelEvent;
|
||||
use super::{DockArea, DockItem};
|
||||
|
||||
pub enum PanelEvent {
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
LayoutChanged,
|
||||
}
|
||||
|
||||
pub struct TitleStyle {
|
||||
pub background: Hsla,
|
||||
|
|
@ -11,6 +23,12 @@ pub struct TitleStyle {
|
|||
}
|
||||
|
||||
pub trait Panel: EventEmitter<PanelEvent> + FocusableView {
|
||||
/// The name of the panel used to serialize, deserialize and identify the panel.
|
||||
///
|
||||
/// This is used to identify the panel when deserializing the panel.
|
||||
/// Once you have defined a panel name, this must not be changed.
|
||||
fn panel_name(&self) -> &'static str;
|
||||
|
||||
/// The title of the panel, default is `None`.
|
||||
fn title(&self, _cx: &WindowContext) -> SharedString {
|
||||
t!("Dock.Unnamed").into()
|
||||
|
|
@ -30,6 +48,9 @@ pub trait Panel: EventEmitter<PanelEvent> + FocusableView {
|
|||
fn popup_menu(&self, this: PopupMenu, _cx: &WindowContext) -> PopupMenu {
|
||||
this
|
||||
}
|
||||
|
||||
/// Dump the panel, used to serialize the panel.
|
||||
fn dump(&self, cx: &AppContext) -> DockItemState;
|
||||
}
|
||||
|
||||
pub trait PanelView: 'static + Send + Sync {
|
||||
|
|
@ -42,6 +63,8 @@ pub trait PanelView: 'static + Send + Sync {
|
|||
fn popup_menu(&self, menu: PopupMenu, cx: &WindowContext) -> PopupMenu;
|
||||
|
||||
fn view(&self) -> AnyView;
|
||||
|
||||
fn dump(&self, cx: &AppContext) -> DockItemState;
|
||||
}
|
||||
|
||||
impl<T: Panel> PanelView for View<T> {
|
||||
|
|
@ -64,6 +87,10 @@ impl<T: Panel> PanelView for View<T> {
|
|||
fn view(&self) -> AnyView {
|
||||
self.clone().into()
|
||||
}
|
||||
|
||||
fn dump(&self, cx: &AppContext) -> DockItemState {
|
||||
self.read(cx).dump(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&dyn PanelView> for AnyView {
|
||||
|
|
@ -83,3 +110,165 @@ impl PartialEq for dyn PanelView {
|
|||
self.view() == other.view()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DockItemState {
|
||||
pub panel_name: String,
|
||||
pub children: Vec<DockItemState>,
|
||||
pub info: DockItemInfo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum DockItemInfo {
|
||||
#[serde(rename = "stack")]
|
||||
Stack {
|
||||
sizes: Vec<Pixels>,
|
||||
/// The axis of the stack, 0 is horizontal, 1 is vertical
|
||||
axis: usize,
|
||||
},
|
||||
#[serde(rename = "tabs")]
|
||||
Tabs { active_index: usize },
|
||||
#[serde(rename = "custom")]
|
||||
Custom(serde_json::Value),
|
||||
}
|
||||
|
||||
impl DockItemInfo {
|
||||
pub fn stack(sizes: Vec<Pixels>, axis: Axis) -> Self {
|
||||
Self::Stack {
|
||||
sizes,
|
||||
axis: if axis == Axis::Horizontal { 0 } else { 1 },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tabs(active_index: usize) -> Self {
|
||||
Self::Tabs { active_index }
|
||||
}
|
||||
|
||||
pub fn custom(value: serde_json::Value) -> Self {
|
||||
Self::Custom(value)
|
||||
}
|
||||
|
||||
pub fn axis(&self) -> Option<Axis> {
|
||||
match self {
|
||||
Self::Stack { axis, .. } => Some(if *axis == 0 {
|
||||
Axis::Horizontal
|
||||
} else {
|
||||
Axis::Vertical
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sizes(&self) -> Option<&Vec<Pixels>> {
|
||||
match self {
|
||||
Self::Stack { sizes, .. } => Some(sizes),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn active_index(&self) -> Option<usize> {
|
||||
match self {
|
||||
Self::Tabs { active_index } => Some(*active_index),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DockItemState {
|
||||
pub fn new(panel_name: &str) -> Self {
|
||||
Self {
|
||||
panel_name: panel_name.to_string(),
|
||||
children: Vec::new(),
|
||||
info: DockItemInfo::Tabs { active_index: 0 },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_child(&mut self, panel: DockItemState) {
|
||||
self.children.push(panel);
|
||||
}
|
||||
|
||||
pub fn to_item(&self, dock_area: WeakView<DockArea>, cx: &mut WindowContext) -> DockItem {
|
||||
// TODO: Use the empty panel if the panel is not registered, for the compatibility.
|
||||
|
||||
let info = self.info.clone();
|
||||
let f = *cx
|
||||
.global::<PanelRegistry>()
|
||||
.items
|
||||
.get(&self.panel_name)
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"The {} panel type is not registed in PanelRegistry.",
|
||||
self.panel_name
|
||||
)
|
||||
});
|
||||
|
||||
let items: Vec<DockItem> = self
|
||||
.children
|
||||
.iter()
|
||||
.map(|child| child.to_item(dock_area.clone(), cx))
|
||||
.collect();
|
||||
|
||||
match info {
|
||||
DockItemInfo::Stack { sizes, axis } => {
|
||||
let axis = if axis == 0 {
|
||||
Axis::Horizontal
|
||||
} else {
|
||||
Axis::Vertical
|
||||
};
|
||||
let sizes = sizes.iter().map(|s| Some(*s)).collect_vec();
|
||||
DockItem::split_with_sizes(axis, items, sizes, &dock_area, cx)
|
||||
}
|
||||
DockItemInfo::Tabs { active_index } => {
|
||||
if items.len() == 1 {
|
||||
return items[0].clone();
|
||||
}
|
||||
|
||||
let items = items
|
||||
.iter()
|
||||
.flat_map(|item| match item {
|
||||
DockItem::Tabs { items, .. } => items.clone(),
|
||||
_ => {
|
||||
unreachable!("Invalid DockItem type in DockItemInfo::Tabs")
|
||||
}
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
DockItem::tabs(items, Some(active_index), &dock_area, cx)
|
||||
}
|
||||
DockItemInfo::Custom(_) => {
|
||||
let view = f(dock_area.clone(), info.clone(), cx);
|
||||
DockItem::tabs(vec![view.into()], None, &dock_area, cx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PanelRegistry {
|
||||
items: HashMap<
|
||||
String,
|
||||
fn(WeakView<DockArea>, DockItemInfo, &mut WindowContext) -> Box<dyn PanelView>,
|
||||
>,
|
||||
}
|
||||
impl PanelRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
items: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Global for PanelRegistry {}
|
||||
|
||||
/// Register the Panel init by panel_name to global registry.
|
||||
pub fn register_panel(
|
||||
cx: &mut AppContext,
|
||||
panel_name: &str,
|
||||
deserialize: fn(WeakView<DockArea>, DockItemInfo, &mut WindowContext) -> Box<dyn PanelView>,
|
||||
) {
|
||||
if let None = cx.try_global::<PanelRegistry>() {
|
||||
cx.set_global(PanelRegistry::new());
|
||||
}
|
||||
|
||||
cx.global_mut::<PanelRegistry>()
|
||||
.items
|
||||
.insert(panel_name.to_string(), deserialize);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
dock::DockItemInfo,
|
||||
h_flex,
|
||||
resizable::{h_resizable, resizable_panel, v_resizable, ResizablePanel, ResizablePanelGroup},
|
||||
resizable::{
|
||||
h_resizable, resizable_panel, v_resizable, ResizablePanel, ResizablePanelEvent,
|
||||
ResizablePanelGroup,
|
||||
},
|
||||
theme::ActiveTheme,
|
||||
Placement,
|
||||
};
|
||||
|
||||
use super::{DockArea, Panel, PanelEvent, PanelView, TabPanel};
|
||||
use super::{register_panel, DockArea, DockItemState, Panel, PanelEvent, PanelView, TabPanel};
|
||||
use gpui::{
|
||||
prelude::FluentBuilder as _, AppContext, Axis, DismissEvent, EventEmitter, FocusHandle,
|
||||
FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, View, ViewContext,
|
||||
|
|
@ -15,6 +19,14 @@ use gpui::{
|
|||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
register_panel(cx, "StackPanel", |_, info, cx| {
|
||||
let axis = info.axis().unwrap_or(Axis::Horizontal);
|
||||
let view = cx.new_view(|cx| StackPanel::new(axis, cx));
|
||||
Box::new(view)
|
||||
})
|
||||
}
|
||||
|
||||
pub struct StackPanel {
|
||||
pub(super) parent: Option<View<StackPanel>>,
|
||||
pub(super) axis: Axis,
|
||||
|
|
@ -24,25 +36,48 @@ pub struct StackPanel {
|
|||
}
|
||||
|
||||
impl Panel for StackPanel {
|
||||
fn panel_name(&self) -> &'static str {
|
||||
"StackPanel"
|
||||
}
|
||||
|
||||
fn title(&self, _cx: &gpui::WindowContext) -> gpui::SharedString {
|
||||
"StackPanel".into()
|
||||
}
|
||||
|
||||
fn dump(&self, cx: &AppContext) -> DockItemState {
|
||||
let sizes = self.panel_group.read(cx).sizes();
|
||||
let mut state = DockItemState::new(self.panel_name());
|
||||
for panel in &self.panels {
|
||||
state.add_child(panel.dump(cx));
|
||||
state.info = DockItemInfo::stack(sizes.clone(), self.axis);
|
||||
}
|
||||
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
impl StackPanel {
|
||||
pub fn new(axis: Axis, cx: &mut ViewContext<Self>) -> Self {
|
||||
let panel_group = cx.new_view(|cx| {
|
||||
if axis == Axis::Horizontal {
|
||||
h_resizable(cx)
|
||||
} else {
|
||||
v_resizable(cx)
|
||||
}
|
||||
});
|
||||
|
||||
// Bubble up the resize event.
|
||||
cx.subscribe(&panel_group, |_, _, _: &ResizablePanelEvent, cx| {
|
||||
cx.emit(PanelEvent::LayoutChanged)
|
||||
})
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
axis,
|
||||
parent: None,
|
||||
focus_handle: cx.focus_handle(),
|
||||
panels: SmallVec::new(),
|
||||
panel_group: cx.new_view(|cx| {
|
||||
if axis == Axis::Horizontal {
|
||||
h_resizable(cx)
|
||||
} else {
|
||||
v_resizable(cx)
|
||||
}
|
||||
}),
|
||||
panel_group,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,9 +201,10 @@ impl StackPanel {
|
|||
|
||||
self.panels.insert(ix, panel.clone());
|
||||
self.panel_group.update(cx, |view, cx| {
|
||||
view.insert_child(Self::new_resizable_panel(panel, size), ix, cx)
|
||||
view.insert_child(Self::new_resizable_panel(panel.clone(), size), ix, cx)
|
||||
});
|
||||
|
||||
cx.emit(PanelEvent::LayoutChanged);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
|
@ -180,6 +216,7 @@ impl StackPanel {
|
|||
view.remove_child(ix, cx);
|
||||
});
|
||||
|
||||
cx.emit(PanelEvent::LayoutChanged);
|
||||
self.remove_self_if_empty(cx);
|
||||
} else {
|
||||
println!("Panel not found in stack panel.");
|
||||
|
|
@ -202,6 +239,7 @@ impl StackPanel {
|
|||
cx,
|
||||
);
|
||||
});
|
||||
cx.emit(PanelEvent::LayoutChanged);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -218,10 +256,11 @@ impl StackPanel {
|
|||
let view = cx.view().clone();
|
||||
if let Some(parent) = self.parent.as_ref() {
|
||||
parent.update(cx, |parent, cx| {
|
||||
parent.remove_panel(Arc::new(view), cx);
|
||||
parent.remove_panel(Arc::new(view.clone()), cx);
|
||||
});
|
||||
}
|
||||
|
||||
cx.emit(PanelEvent::LayoutChanged);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use rust_i18n::t;
|
|||
|
||||
use crate::{
|
||||
button::Button,
|
||||
dock::DockItemInfo,
|
||||
h_flex,
|
||||
popup_menu::{PopupMenu, PopupMenuExt},
|
||||
tab::{Tab, TabBar},
|
||||
|
|
@ -18,12 +19,16 @@ use crate::{
|
|||
v_flex, AxisExt, IconName, Placement, Selectable, Sizable,
|
||||
};
|
||||
|
||||
use super::{ClosePanel, DockArea, Panel, PanelView, StackPanel, ToggleZoom};
|
||||
use super::{
|
||||
register_panel, ClosePanel, DockArea, DockItemState, Panel, PanelEvent, PanelView, StackPanel,
|
||||
ToggleZoom,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum PanelEvent {
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
register_panel(cx, "TabPanel", |dock_area, _, cx| {
|
||||
let view = cx.new_view(|cx| TabPanel::new(None, dock_area, cx));
|
||||
Box::new(view)
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
@ -72,6 +77,41 @@ pub struct TabPanel {
|
|||
will_split_placement: Option<Placement>,
|
||||
}
|
||||
|
||||
impl Panel for TabPanel {
|
||||
fn panel_name(&self) -> &'static str {
|
||||
"TabPanel"
|
||||
}
|
||||
|
||||
fn title(&self, cx: &WindowContext) -> gpui::SharedString {
|
||||
self.active_panel()
|
||||
.map(|panel| panel.title(cx))
|
||||
.unwrap_or("Empty Tab".into())
|
||||
}
|
||||
|
||||
fn closeable(&self, cx: &WindowContext) -> bool {
|
||||
self.active_panel()
|
||||
.map(|panel| panel.closeable(cx))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn popup_menu(&self, menu: PopupMenu, cx: &WindowContext) -> PopupMenu {
|
||||
if let Some(panel) = self.active_panel() {
|
||||
panel.popup_menu(menu, cx)
|
||||
} else {
|
||||
menu
|
||||
}
|
||||
}
|
||||
|
||||
fn dump(&self, cx: &AppContext) -> DockItemState {
|
||||
let mut state = DockItemState::new(self.panel_name());
|
||||
for panel in self.panels.iter() {
|
||||
state.add_child(panel.dump(cx));
|
||||
state.info = DockItemInfo::tabs(self.active_ix);
|
||||
}
|
||||
state
|
||||
}
|
||||
}
|
||||
|
||||
impl TabPanel {
|
||||
pub fn new(
|
||||
stack_panel: Option<View<StackPanel>>,
|
||||
|
|
@ -102,6 +142,7 @@ impl TabPanel {
|
|||
fn set_active_ix(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
|
||||
self.active_ix = ix;
|
||||
self.tab_bar_scroll_handle.scroll_to_item(ix);
|
||||
cx.emit(PanelEvent::LayoutChanged);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
|
@ -118,6 +159,7 @@ impl TabPanel {
|
|||
self.panels.push(panel);
|
||||
// set the active panel to the new panel
|
||||
self.set_active_ix(self.panels.len() - 1, cx);
|
||||
cx.emit(PanelEvent::LayoutChanged);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
|
@ -140,6 +182,7 @@ impl TabPanel {
|
|||
.ok()
|
||||
})
|
||||
.detach();
|
||||
cx.emit(PanelEvent::LayoutChanged);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
|
@ -159,13 +202,15 @@ impl TabPanel {
|
|||
|
||||
self.panels.insert(ix, panel);
|
||||
self.set_active_ix(ix, cx);
|
||||
cx.emit(PanelEvent::LayoutChanged);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Remove a panel from the tab panel
|
||||
pub fn remove_panel(&mut self, panel: Arc<dyn PanelView>, cx: &mut ViewContext<Self>) {
|
||||
self.detach_panel(panel, cx);
|
||||
self.remove_self_if_empty(cx)
|
||||
self.remove_self_if_empty(cx);
|
||||
cx.emit(PanelEvent::LayoutChanged);
|
||||
}
|
||||
|
||||
fn detach_panel(&mut self, panel: Arc<dyn PanelView>, cx: &mut ViewContext<Self>) {
|
||||
|
|
@ -443,6 +488,7 @@ impl TabPanel {
|
|||
}
|
||||
|
||||
self.remove_self_if_empty(cx);
|
||||
cx.emit(PanelEvent::LayoutChanged);
|
||||
}
|
||||
|
||||
/// Add panel with split placement
|
||||
|
|
@ -534,6 +580,8 @@ impl TabPanel {
|
|||
})
|
||||
.detach()
|
||||
}
|
||||
|
||||
cx.emit(PanelEvent::LayoutChanged);
|
||||
}
|
||||
|
||||
fn on_action_toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
|
||||
|
|
@ -552,27 +600,6 @@ impl TabPanel {
|
|||
}
|
||||
}
|
||||
|
||||
impl Panel for TabPanel {
|
||||
fn title(&self, cx: &WindowContext) -> gpui::SharedString {
|
||||
self.active_panel()
|
||||
.map(|panel| panel.title(cx))
|
||||
.unwrap_or("Empty Tab".into())
|
||||
}
|
||||
|
||||
fn closeable(&self, cx: &WindowContext) -> bool {
|
||||
self.active_panel()
|
||||
.map(|panel| panel.closeable(cx))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn popup_menu(&self, menu: PopupMenu, cx: &WindowContext) -> PopupMenu {
|
||||
if let Some(panel) = self.active_panel() {
|
||||
panel.popup_menu(menu, cx)
|
||||
} else {
|
||||
menu
|
||||
}
|
||||
}
|
||||
}
|
||||
impl FocusableView for TabPanel {
|
||||
fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
|
|
|
|||
|
|
@ -58,13 +58,14 @@ pub use svg_img::*;
|
|||
|
||||
/// Initialize the UI module.
|
||||
pub fn init(cx: &mut gpui::AppContext) {
|
||||
context_menu::init(cx);
|
||||
date_picker::init(cx);
|
||||
dock::init(cx);
|
||||
dropdown::init(cx);
|
||||
input::init(cx);
|
||||
list::init(cx);
|
||||
dropdown::init(cx);
|
||||
date_picker::init(cx);
|
||||
popover::init(cx);
|
||||
popup_menu::init(cx);
|
||||
context_menu::init(cx);
|
||||
table::init(cx);
|
||||
webview::init(cx)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@ use std::rc::Rc;
|
|||
|
||||
use gpui::{
|
||||
canvas, div, prelude::FluentBuilder, px, Along, AnyElement, AnyView, Axis, Bounds, Element,
|
||||
Entity, EntityId, InteractiveElement as _, IntoElement, MouseMoveEvent, MouseUpEvent,
|
||||
ParentElement, Pixels, Render, StatefulInteractiveElement, Style, Styled, View, ViewContext,
|
||||
VisualContext as _, WindowContext,
|
||||
Entity, EntityId, EventEmitter, InteractiveElement as _, IntoElement, MouseMoveEvent,
|
||||
MouseUpEvent, ParentElement, Pixels, Render, StatefulInteractiveElement, Style, Styled, View,
|
||||
ViewContext, VisualContext as _, WindowContext,
|
||||
};
|
||||
|
||||
use crate::{h_flex, theme::ActiveTheme, v_flex, AxisExt};
|
||||
|
|
@ -12,6 +12,10 @@ use crate::{h_flex, theme::ActiveTheme, v_flex, AxisExt};
|
|||
const PANEL_MIN_SIZE: Pixels = px(100.);
|
||||
const HANDLE_PADDING: Pixels = px(4.);
|
||||
|
||||
pub enum ResizablePanelEvent {
|
||||
Resized,
|
||||
}
|
||||
|
||||
#[derive(Clone, Render)]
|
||||
pub struct DragPanel(pub (EntityId, usize, Axis));
|
||||
|
||||
|
|
@ -89,6 +93,11 @@ impl ResizablePanelGroup {
|
|||
self
|
||||
}
|
||||
|
||||
/// Returns the sizes of the resizable panels.
|
||||
pub(crate) fn sizes(&self) -> Vec<Pixels> {
|
||||
self.sizes.clone()
|
||||
}
|
||||
|
||||
pub fn add_child(&mut self, panel: ResizablePanel, cx: &mut ViewContext<Self>) {
|
||||
let mut panel = panel;
|
||||
panel.axis = self.axis;
|
||||
|
|
@ -189,6 +198,11 @@ impl ResizablePanelGroup {
|
|||
)
|
||||
}
|
||||
|
||||
fn done_resizing(&mut self, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(ResizablePanelEvent::Resized);
|
||||
self.resizing_panel_ix = None;
|
||||
}
|
||||
|
||||
fn sync_real_panel_sizes(&mut self, cx: &WindowContext) {
|
||||
for (i, panel) in self.panels.iter().enumerate() {
|
||||
self.sizes[i] = panel.read(cx).bounds.size.along(self.axis)
|
||||
|
|
@ -256,7 +270,7 @@ impl ResizablePanelGroup {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ResizablePanelEvent> for ResizablePanelGroup {}
|
||||
impl Render for ResizablePanelGroup {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let view = cx.view().clone();
|
||||
|
|
@ -477,7 +491,7 @@ impl Element for ResizePanelGroupElement {
|
|||
let view = self.view.clone();
|
||||
move |_: &MouseUpEvent, phase, cx| {
|
||||
if phase.bubble() {
|
||||
view.update(cx, |view, _| view.resizing_panel_ix = None);
|
||||
view.update(cx, |view, cx| view.done_resizing(cx));
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue