Add to support dump/load Dock layout. (#226)

This commit is contained in:
Jason Lee 2024-09-09 13:30:22 +08:00 committed by GitHub
parent 2f80ba408a
commit bf134b2ff9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 529 additions and 117 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
/target /target
.DS_Store .DS_Store
layout.json

2
Cargo.lock generated
View file

@ -2294,6 +2294,7 @@ dependencies = [
"log", "log",
"rust-embed", "rust-embed",
"serde", "serde",
"serde_json",
"story", "story",
"ui", "ui",
"workspace", "workspace",
@ -4854,6 +4855,7 @@ dependencies = [
"gpui", "gpui",
"regex", "regex",
"serde", "serde",
"serde_json",
"ui", "ui",
] ]

View file

@ -12,6 +12,7 @@ workspace.workspace = true
ui.workspace = true ui.workspace = true
story.workspace = true story.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true
[lints] [lints]
workspace = true workspace = true

View file

@ -1,18 +1,17 @@
use anyhow::Result;
use gpui::*; use gpui::*;
use prelude::FluentBuilder as _; use prelude::FluentBuilder as _;
use private::serde::Deserialize; use private::serde::Deserialize;
use std::sync::Arc;
use story::{ use story::{
ButtonStory, CalendarStory, DropdownStory, IconStory, ImageStory, InputStory, ListStory, ButtonStory, CalendarStory, DropdownStory, IconStory, ImageStory, InputStory, ListStory,
ModalStory, PopupStory, ProgressStory, ResizableStory, ScrollableStory, StoryContainer, ModalStory, PopupStory, ProgressStory, ResizableStory, ScrollableStory, StoryContainer,
SwitchStory, TableStory, TextStory, TooltipStory, SwitchStory, TableStory, TextStory, TooltipStory,
}; };
use workspace::TitleBar;
use std::sync::Arc;
use ui::{ use ui::{
button::Button, button::Button,
color_picker::{ColorPicker, ColorPickerEvent}, color_picker::{ColorPicker, ColorPickerEvent},
dock::{DockArea, DockItem}, dock::{DockArea, DockEvent, DockItem, DockItemState},
drawer::Drawer, drawer::Drawer,
h_flex, h_flex,
modal::Modal, modal::Modal,
@ -20,6 +19,7 @@ use ui::{
theme::{ActiveTheme, Colorize as _, Theme}, theme::{ActiveTheme, Colorize as _, Theme},
ContextModal, IconName, Root, Sizable, ContextModal, IconName, Root, Sizable,
}; };
use workspace::TitleBar;
use crate::app_state::AppState; use crate::app_state::AppState;
@ -52,8 +52,73 @@ impl StoryWorkspace {
.detach(); .detach();
let dock_area = cx.new_view(|cx| DockArea::new("main-dock", cx)); 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, Axis::Horizontal,
vec![ vec![
DockItem::split( DockItem::split(
@ -116,41 +181,7 @@ impl StoryWorkspace {
vec![Some(px(300.)), None, Some(px(350.))], vec![Some(px(300.)), None, Some(px(350.))],
&dock_area, &dock_area,
cx, 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( pub fn new_local(

View file

@ -11,6 +11,7 @@ charts-rs = "0.3"
regex = "1" regex = "1"
chrono = "0.4" chrono = "0.4"
serde = "1" serde = "1"
serde_json = "1"
[lints] [lints]
workspace = true workspace = true

View file

@ -28,6 +28,7 @@ pub use popup_story::PopupStory;
pub use progress_story::ProgressStory; pub use progress_story::ProgressStory;
pub use resizable_story::ResizableStory; pub use resizable_story::ResizableStory;
pub use scrollable_story::ScrollableStory; pub use scrollable_story::ScrollableStory;
use serde::{Deserialize, Serialize};
pub use switch_story::SwitchStory; pub use switch_story::SwitchStory;
pub use table_story::TableStory; pub use table_story::TableStory;
pub use text_story::TextStory; pub use text_story::TextStory;
@ -42,7 +43,7 @@ use gpui::{
use ui::{ use ui::{
divider::Divider, divider::Divider,
dock::{Panel, PanelEvent, TitleStyle}, dock::{register_panel, DockItemInfo, DockItemState, Panel, PanelEvent, TitleStyle},
h_flex, h_flex,
label::Label, label::Label,
notification::Notification, notification::Notification,
@ -55,6 +56,24 @@ pub fn init(cx: &mut AppContext) {
input_story::init(cx); input_story::init(cx);
dropdown_story::init(cx); dropdown_story::init(cx);
popup_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]); actions!(story, [PanelInfo]);
@ -95,7 +114,7 @@ pub enum ContainerEvent {
pub trait Story { pub trait Story {
fn klass() -> &'static str { fn klass() -> &'static str {
std::any::type_name::<Self>() std::any::type_name::<Self>().split("::").last().unwrap()
} }
fn title() -> &'static str; fn title() -> &'static str;
@ -114,7 +133,7 @@ pub trait Story {
impl EventEmitter<ContainerEvent> for StoryContainer {} impl EventEmitter<ContainerEvent> for StoryContainer {}
impl 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(); let focus_handle = cx.focus_handle();
Self { Self {
@ -126,7 +145,7 @@ impl StoryContainer {
height: None, height: None,
story: None, story: None,
story_klass: None, story_klass: None,
closeable, closeable: true,
} }
} }
@ -138,7 +157,8 @@ impl StoryContainer {
let story_klass = S::klass(); let story_klass = S::klass();
let view = cx.new_view(|cx| { 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.name = name.into();
story.description = description.into(); story.description = description.into();
story.title_bg = S::title_bg(); 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 { impl Panel for StoryContainer {
fn panel_name(&self) -> &'static str {
"StoryContainer"
}
fn title(&self, _cx: &WindowContext) -> SharedString { fn title(&self, _cx: &WindowContext) -> SharedString {
self.name.clone() self.name.clone()
} }
@ -196,6 +272,15 @@ impl Panel for StoryContainer {
menu.track_focus(&self.focus_handle) menu.track_focus(&self.focus_handle)
.menu("Info", Box::new(PanelInfo)) .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 {} impl EventEmitter<PanelEvent> for StoryContainer {}

View file

@ -5,16 +5,29 @@ mod tab_panel;
use std::sync::Arc; use std::sync::Arc;
use gpui::{ use gpui::{
actions, div, prelude::FluentBuilder, AnyElement, AnyView, Axis, InteractiveElement as _, actions, div, prelude::FluentBuilder, AnyElement, AnyView, AppContext, Axis, EventEmitter,
IntoElement, ParentElement as _, Pixels, Render, SharedString, Styled, View, ViewContext, InteractiveElement as _, IntoElement, ParentElement as _, Pixels, Render, SharedString, Styled,
VisualContext, WindowContext, View, ViewContext, VisualContext, WeakView, WindowContext,
}; };
pub use panel::*; pub use panel::*;
pub use stack_panel::*; pub use stack_panel::*;
pub use tab_panel::*; pub use tab_panel::*;
pub fn init(cx: &mut AppContext) {
stack_panel::init(cx);
tab_panel::init(cx);
}
actions!(dock, [ToggleZoom, ClosePanel]); 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. /// The main area of the dock.
pub struct DockArea { pub struct DockArea {
id: SharedString, id: SharedString,
@ -43,7 +56,7 @@ impl DockItem {
pub fn split( pub fn split(
axis: Axis, axis: Axis,
items: Vec<DockItem>, items: Vec<DockItem>,
dock_area: &View<DockArea>, dock_area: &WeakView<DockArea>,
cx: &mut WindowContext, cx: &mut WindowContext,
) -> Self { ) -> Self {
let sizes = vec![None; items.len()]; let sizes = vec![None; items.len()];
@ -58,7 +71,7 @@ impl DockItem {
axis: Axis, axis: Axis,
items: Vec<DockItem>, items: Vec<DockItem>,
sizes: Vec<Option<Pixels>>, sizes: Vec<Option<Pixels>>,
dock_area: &View<DockArea>, dock_area: &WeakView<DockArea>,
cx: &mut WindowContext, cx: &mut WindowContext,
) -> Self { ) -> Self {
let mut items = items; let mut items = items;
@ -67,13 +80,13 @@ impl DockItem {
for (i, item) in items.iter_mut().enumerate() { for (i, item) in items.iter_mut().enumerate() {
let view = item.view(); let view = item.view();
let size = sizes.get(i).copied().flatten(); 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() { for (i, item) in items.iter().enumerate() {
let view = item.view(); let view = item.view();
let size = sizes.get(i).copied().flatten(); 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 stack_panel
@ -92,7 +105,7 @@ impl DockItem {
pub fn tabs( pub fn tabs(
items: Vec<Arc<dyn PanelView>>, items: Vec<Arc<dyn PanelView>>,
active_ix: Option<usize>, active_ix: Option<usize>,
dock_area: &View<DockArea>, dock_area: &WeakView<DockArea>,
cx: &mut WindowContext, cx: &mut WindowContext,
) -> Self { ) -> Self {
let mut new_items: Vec<Arc<dyn PanelView>> = vec![]; let mut new_items: Vec<Arc<dyn PanelView>> = vec![];
@ -104,7 +117,7 @@ impl DockItem {
pub fn tab<P: Panel>( pub fn tab<P: Panel>(
item: View<P>, item: View<P>,
dock_area: &View<DockArea>, dock_area: &WeakView<DockArea>,
cx: &mut WindowContext, cx: &mut WindowContext,
) -> Self { ) -> Self {
Self::new_tabs(vec![Arc::new(item.clone())], None, dock_area, cx) Self::new_tabs(vec![Arc::new(item.clone())], None, dock_area, cx)
@ -113,16 +126,16 @@ impl DockItem {
fn new_tabs( fn new_tabs(
items: Vec<Arc<dyn PanelView>>, items: Vec<Arc<dyn PanelView>>,
active_ix: Option<usize>, active_ix: Option<usize>,
dock_area: &View<DockArea>, dock_area: &WeakView<DockArea>,
cx: &mut WindowContext, cx: &mut WindowContext,
) -> Self { ) -> Self {
let active_ix = active_ix.unwrap_or(0); let active_ix = active_ix.unwrap_or(0);
let tab_panel = cx.new_view(|cx| { 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() { for item in items.iter() {
tab_panel.add_panel(item.clone(), cx) tab_panel.add_panel(item.clone(), cx)
} }
tab_panel.active_ix = active_ix;
tab_panel tab_panel
}); });
@ -178,20 +191,21 @@ impl DockArea {
cx.notify(); 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 /// Subscribe event on the panels
#[allow(clippy::only_used_in_recursion)] #[allow(clippy::only_used_in_recursion)]
fn subscribe_item(&self, item: &DockItem, cx: &mut ViewContext<Self>) { fn subscribe_item(&self, item: &DockItem, cx: &mut ViewContext<Self>) {
let dock_area = cx.view();
/// Subscribe zoom event on the panel /// Subscribe zoom event on the panel
fn subscribe_zoom<P: Panel>( fn subscribe_zoom<P: Panel>(view: &View<P>, cx: &mut ViewContext<DockArea>) {
view: &View<P>,
dock_area: View<DockArea>,
cx: &mut ViewContext<DockArea>,
) {
cx.subscribe(view, move |_, panel, event, cx| match event { cx.subscribe(view, move |_, panel, event, cx| match event {
PanelEvent::ZoomIn => { PanelEvent::ZoomIn => {
let dock_area = dock_area.clone(); let dock_area = cx.view().clone();
let panel = panel.clone(); let panel = panel.clone();
cx.spawn(|_, mut cx| async move { cx.spawn(|_, mut cx| async move {
let _ = cx.update(|cx| { let _ = cx.update(|cx| {
@ -204,7 +218,7 @@ impl DockArea {
.detach(); .detach();
} }
PanelEvent::ZoomOut => { PanelEvent::ZoomOut => {
let dock_area = dock_area.clone(); let dock_area = cx.view().clone();
cx.spawn(|_, mut cx| async move { cx.spawn(|_, mut cx| async move {
let _ = cx.update(|cx| { let _ = cx.update(|cx| {
let _ = dock_area.update(cx, |view, cx| view.set_zoomed_out(cx)); let _ = dock_area.update(cx, |view, cx| view.set_zoomed_out(cx));
@ -212,20 +226,27 @@ impl DockArea {
}) })
.detach() .detach()
} }
PanelEvent::LayoutChanged => cx.emit(DockEvent::LayoutChanged),
}) })
.detach(); .detach();
} }
match item { match item {
DockItem::Split { items, .. } => { DockItem::Split { items, view, .. } => {
for item in items { for item in items {
self.subscribe_item(item, cx); self.subscribe_item(item, cx);
} }
cx.subscribe(view, move |_, _, event, cx| match event {
PanelEvent::LayoutChanged => cx.emit(DockEvent::LayoutChanged),
_ => {}
})
.detach();
} }
DockItem::Tabs { view, .. } => { DockItem::Tabs { view, .. } => {
// We need, only subscribe to the zoom events on the TabPanel // We need, only subscribe to the zoom events on the TabPanel
// Because we always wrap the DockItem::Panel in a DockItem::Tabs // 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 { impl Render for DockArea {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
// println!("Rendering dock area"); // println!("Rendering dock area");

View file

@ -1,9 +1,21 @@
use gpui::{AnyView, EventEmitter, FocusableView, Hsla, SharedString, View, WindowContext}; use std::collections::HashMap;
use rust_i18n::t;
use crate::popup_menu::PopupMenu; 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 struct TitleStyle {
pub background: Hsla, pub background: Hsla,
@ -11,6 +23,12 @@ pub struct TitleStyle {
} }
pub trait Panel: EventEmitter<PanelEvent> + FocusableView { 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`. /// The title of the panel, default is `None`.
fn title(&self, _cx: &WindowContext) -> SharedString { fn title(&self, _cx: &WindowContext) -> SharedString {
t!("Dock.Unnamed").into() t!("Dock.Unnamed").into()
@ -30,6 +48,9 @@ pub trait Panel: EventEmitter<PanelEvent> + FocusableView {
fn popup_menu(&self, this: PopupMenu, _cx: &WindowContext) -> PopupMenu { fn popup_menu(&self, this: PopupMenu, _cx: &WindowContext) -> PopupMenu {
this this
} }
/// Dump the panel, used to serialize the panel.
fn dump(&self, cx: &AppContext) -> DockItemState;
} }
pub trait PanelView: 'static + Send + Sync { 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 popup_menu(&self, menu: PopupMenu, cx: &WindowContext) -> PopupMenu;
fn view(&self) -> AnyView; fn view(&self) -> AnyView;
fn dump(&self, cx: &AppContext) -> DockItemState;
} }
impl<T: Panel> PanelView for View<T> { impl<T: Panel> PanelView for View<T> {
@ -64,6 +87,10 @@ impl<T: Panel> PanelView for View<T> {
fn view(&self) -> AnyView { fn view(&self) -> AnyView {
self.clone().into() self.clone().into()
} }
fn dump(&self, cx: &AppContext) -> DockItemState {
self.read(cx).dump(cx)
}
} }
impl From<&dyn PanelView> for AnyView { impl From<&dyn PanelView> for AnyView {
@ -83,3 +110,165 @@ impl PartialEq for dyn PanelView {
self.view() == other.view() 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);
}

View file

@ -1,13 +1,17 @@
use std::sync::Arc; use std::sync::Arc;
use crate::{ use crate::{
dock::DockItemInfo,
h_flex, h_flex,
resizable::{h_resizable, resizable_panel, v_resizable, ResizablePanel, ResizablePanelGroup}, resizable::{
h_resizable, resizable_panel, v_resizable, ResizablePanel, ResizablePanelEvent,
ResizablePanelGroup,
},
theme::ActiveTheme, theme::ActiveTheme,
Placement, Placement,
}; };
use super::{DockArea, Panel, PanelEvent, PanelView, TabPanel}; use super::{register_panel, DockArea, DockItemState, Panel, PanelEvent, PanelView, TabPanel};
use gpui::{ use gpui::{
prelude::FluentBuilder as _, AppContext, Axis, DismissEvent, EventEmitter, FocusHandle, prelude::FluentBuilder as _, AppContext, Axis, DismissEvent, EventEmitter, FocusHandle,
FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, View, ViewContext, FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, View, ViewContext,
@ -15,6 +19,14 @@ use gpui::{
}; };
use smallvec::SmallVec; 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 struct StackPanel {
pub(super) parent: Option<View<StackPanel>>, pub(super) parent: Option<View<StackPanel>>,
pub(super) axis: Axis, pub(super) axis: Axis,
@ -24,25 +36,48 @@ pub struct StackPanel {
} }
impl Panel for StackPanel { impl Panel for StackPanel {
fn panel_name(&self) -> &'static str {
"StackPanel"
}
fn title(&self, _cx: &gpui::WindowContext) -> gpui::SharedString { fn title(&self, _cx: &gpui::WindowContext) -> gpui::SharedString {
"StackPanel".into() "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 { impl StackPanel {
pub fn new(axis: Axis, cx: &mut ViewContext<Self>) -> Self { 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 { Self {
axis, axis,
parent: None, parent: None,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
panels: SmallVec::new(), panels: SmallVec::new(),
panel_group: cx.new_view(|cx| { panel_group,
if axis == Axis::Horizontal {
h_resizable(cx)
} else {
v_resizable(cx)
}
}),
} }
} }
@ -166,9 +201,10 @@ impl StackPanel {
self.panels.insert(ix, panel.clone()); self.panels.insert(ix, panel.clone());
self.panel_group.update(cx, |view, cx| { 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(); cx.notify();
} }
@ -180,6 +216,7 @@ impl StackPanel {
view.remove_child(ix, cx); view.remove_child(ix, cx);
}); });
cx.emit(PanelEvent::LayoutChanged);
self.remove_self_if_empty(cx); self.remove_self_if_empty(cx);
} else { } else {
println!("Panel not found in stack panel."); println!("Panel not found in stack panel.");
@ -202,6 +239,7 @@ impl StackPanel {
cx, cx,
); );
}); });
cx.emit(PanelEvent::LayoutChanged);
} }
} }
@ -218,10 +256,11 @@ impl StackPanel {
let view = cx.view().clone(); let view = cx.view().clone();
if let Some(parent) = self.parent.as_ref() { if let Some(parent) = self.parent.as_ref() {
parent.update(cx, |parent, cx| { 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(); cx.notify();
} }

View file

@ -10,6 +10,7 @@ use rust_i18n::t;
use crate::{ use crate::{
button::Button, button::Button,
dock::DockItemInfo,
h_flex, h_flex,
popup_menu::{PopupMenu, PopupMenuExt}, popup_menu::{PopupMenu, PopupMenuExt},
tab::{Tab, TabBar}, tab::{Tab, TabBar},
@ -18,12 +19,16 @@ use crate::{
v_flex, AxisExt, IconName, Placement, Selectable, Sizable, 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 fn init(cx: &mut AppContext) {
pub enum PanelEvent { register_panel(cx, "TabPanel", |dock_area, _, cx| {
ZoomIn, let view = cx.new_view(|cx| TabPanel::new(None, dock_area, cx));
ZoomOut, Box::new(view)
})
} }
#[derive(Clone)] #[derive(Clone)]
@ -72,6 +77,41 @@ pub struct TabPanel {
will_split_placement: Option<Placement>, 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 { impl TabPanel {
pub fn new( pub fn new(
stack_panel: Option<View<StackPanel>>, stack_panel: Option<View<StackPanel>>,
@ -102,6 +142,7 @@ impl TabPanel {
fn set_active_ix(&mut self, ix: usize, cx: &mut ViewContext<Self>) { fn set_active_ix(&mut self, ix: usize, cx: &mut ViewContext<Self>) {
self.active_ix = ix; self.active_ix = ix;
self.tab_bar_scroll_handle.scroll_to_item(ix); self.tab_bar_scroll_handle.scroll_to_item(ix);
cx.emit(PanelEvent::LayoutChanged);
cx.notify(); cx.notify();
} }
@ -118,6 +159,7 @@ impl TabPanel {
self.panels.push(panel); self.panels.push(panel);
// set the active panel to the new panel // set the active panel to the new panel
self.set_active_ix(self.panels.len() - 1, cx); self.set_active_ix(self.panels.len() - 1, cx);
cx.emit(PanelEvent::LayoutChanged);
cx.notify(); cx.notify();
} }
@ -140,6 +182,7 @@ impl TabPanel {
.ok() .ok()
}) })
.detach(); .detach();
cx.emit(PanelEvent::LayoutChanged);
cx.notify(); cx.notify();
} }
@ -159,13 +202,15 @@ impl TabPanel {
self.panels.insert(ix, panel); self.panels.insert(ix, panel);
self.set_active_ix(ix, cx); self.set_active_ix(ix, cx);
cx.emit(PanelEvent::LayoutChanged);
cx.notify(); cx.notify();
} }
/// Remove a panel from the tab panel /// Remove a panel from the tab panel
pub fn remove_panel(&mut self, panel: Arc<dyn PanelView>, cx: &mut ViewContext<Self>) { pub fn remove_panel(&mut self, panel: Arc<dyn PanelView>, cx: &mut ViewContext<Self>) {
self.detach_panel(panel, cx); 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>) { 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); self.remove_self_if_empty(cx);
cx.emit(PanelEvent::LayoutChanged);
} }
/// Add panel with split placement /// Add panel with split placement
@ -534,6 +580,8 @@ impl TabPanel {
}) })
.detach() .detach()
} }
cx.emit(PanelEvent::LayoutChanged);
} }
fn on_action_toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) { 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 { impl FocusableView for TabPanel {
fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle { fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
self.focus_handle.clone() self.focus_handle.clone()

View file

@ -58,13 +58,14 @@ pub use svg_img::*;
/// Initialize the UI module. /// Initialize the UI module.
pub fn init(cx: &mut gpui::AppContext) { pub fn init(cx: &mut gpui::AppContext) {
context_menu::init(cx);
date_picker::init(cx);
dock::init(cx);
dropdown::init(cx);
input::init(cx); input::init(cx);
list::init(cx); list::init(cx);
dropdown::init(cx);
date_picker::init(cx);
popover::init(cx); popover::init(cx);
popup_menu::init(cx); popup_menu::init(cx);
context_menu::init(cx);
table::init(cx); table::init(cx);
webview::init(cx) webview::init(cx)
} }

View file

@ -2,9 +2,9 @@ use std::rc::Rc;
use gpui::{ use gpui::{
canvas, div, prelude::FluentBuilder, px, Along, AnyElement, AnyView, Axis, Bounds, Element, canvas, div, prelude::FluentBuilder, px, Along, AnyElement, AnyView, Axis, Bounds, Element,
Entity, EntityId, InteractiveElement as _, IntoElement, MouseMoveEvent, MouseUpEvent, Entity, EntityId, EventEmitter, InteractiveElement as _, IntoElement, MouseMoveEvent,
ParentElement, Pixels, Render, StatefulInteractiveElement, Style, Styled, View, ViewContext, MouseUpEvent, ParentElement, Pixels, Render, StatefulInteractiveElement, Style, Styled, View,
VisualContext as _, WindowContext, ViewContext, VisualContext as _, WindowContext,
}; };
use crate::{h_flex, theme::ActiveTheme, v_flex, AxisExt}; 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 PANEL_MIN_SIZE: Pixels = px(100.);
const HANDLE_PADDING: Pixels = px(4.); const HANDLE_PADDING: Pixels = px(4.);
pub enum ResizablePanelEvent {
Resized,
}
#[derive(Clone, Render)] #[derive(Clone, Render)]
pub struct DragPanel(pub (EntityId, usize, Axis)); pub struct DragPanel(pub (EntityId, usize, Axis));
@ -89,6 +93,11 @@ impl ResizablePanelGroup {
self 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>) { pub fn add_child(&mut self, panel: ResizablePanel, cx: &mut ViewContext<Self>) {
let mut panel = panel; let mut panel = panel;
panel.axis = self.axis; 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) { fn sync_real_panel_sizes(&mut self, cx: &WindowContext) {
for (i, panel) in self.panels.iter().enumerate() { for (i, panel) in self.panels.iter().enumerate() {
self.sizes[i] = panel.read(cx).bounds.size.along(self.axis) 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 { impl Render for ResizablePanelGroup {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let view = cx.view().clone(); let view = cx.view().clone();
@ -477,7 +491,7 @@ impl Element for ResizePanelGroupElement {
let view = self.view.clone(); let view = self.view.clone();
move |_: &MouseUpEvent, phase, cx| { move |_: &MouseUpEvent, phase, cx| {
if phase.bubble() { if phase.bubble() {
view.update(cx, |view, _| view.resizing_panel_ix = None); view.update(cx, |view, cx| view.done_resizing(cx));
} }
} }
}) })