feat: add tiles from gpui-components

This commit is contained in:
2024-12-18 07:54:40 +07:00
parent 0682612d42
commit 407db7a7d5
13 changed files with 1018 additions and 182 deletions

View File

@@ -0,0 +1,9 @@
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="list-filter" transform="translate(16.142767, 16.107233) rotate(-45.000000) translate(-16.142767, -16.107233) translate(3.642767, 10.491117)" stroke="#000000" stroke-width="2">
<line x1="0.454058454" y1="0.48959236" x2="24.1421356" y2="0.843145751" stroke-linecap="square"></line>
<line x1="4.69669914" y1="6.14644661" x2="20.1188954" y2="5.79289322"></line>
<line x1="9.06066017" y1="10.732233" x2="15.3033009" y2="10.3890873"></line>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 730 B

View File

@@ -1,6 +1,6 @@
use coop_ui::{ use coop_ui::{
button::Button, button::Button,
dock::{DockItemState, Panel, PanelEvent, TitleStyle}, dock::{Panel, PanelEvent, PanelState, TitleStyle},
popup_menu::PopupMenu, popup_menu::PopupMenu,
}; };
use form::Form; use form::Form;
@@ -75,8 +75,8 @@ impl Panel for ChatPanel {
vec![] vec![]
} }
fn dump(&self, _cx: &AppContext) -> DockItemState { fn dump(&self, _cx: &AppContext) -> PanelState {
DockItemState::new(self) PanelState::new(self)
} }
} }

View File

@@ -1,6 +1,6 @@
use coop_ui::{ use coop_ui::{
button::Button, button::Button,
dock::{DockItemState, Panel, PanelEvent, TitleStyle}, dock::{Panel, PanelEvent, PanelState, TitleStyle},
popup_menu::PopupMenu, popup_menu::PopupMenu,
scroll::ScrollbarAxis, scroll::ScrollbarAxis,
StyledExt, StyledExt,
@@ -68,8 +68,8 @@ impl Panel for LeftDock {
vec![] vec![]
} }
fn dump(&self, _cx: &AppContext) -> DockItemState { fn dump(&self, _cx: &AppContext) -> PanelState {
DockItemState::new(self) PanelState::new(self)
} }
} }

View File

@@ -1,6 +1,6 @@
use coop_ui::{ use coop_ui::{
button::Button, button::Button,
dock::{DockItemState, Panel, PanelEvent, TitleStyle}, dock::{Panel, PanelEvent, PanelState, TitleStyle},
popup_menu::PopupMenu, popup_menu::PopupMenu,
theme::{ActiveTheme, Colorize}, theme::{ActiveTheme, Colorize},
StyledExt, StyledExt,
@@ -58,8 +58,8 @@ impl Panel for WelcomePanel {
vec![] vec![]
} }
fn dump(&self, _cx: &AppContext) -> DockItemState { fn dump(&self, _cx: &AppContext) -> PanelState {
DockItemState::new(self) PanelState::new(self)
} }
} }

View File

@@ -1,7 +1,3 @@
//! Dock is a fixed container that places at left, bottom, right of the Windows.
use std::sync::Arc;
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, px, Axis, Element, Entity, InteractiveElement as _, div, prelude::FluentBuilder as _, px, Axis, Element, Entity, InteractiveElement as _,
IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render, IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render,
@@ -9,6 +5,7 @@ use gpui::{
WeakView, WindowContext, WeakView, WindowContext,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::sync::Arc;
use crate::{ use crate::{
resizable::{HANDLE_PADDING, HANDLE_SIZE, PANEL_MIN_SIZE}, resizable::{HANDLE_PADDING, HANDLE_SIZE, PANEL_MIN_SIZE},
@@ -193,6 +190,16 @@ impl Dock {
} }
}); });
} }
DockItem::Tiles { view, .. } => {
cx.defer({
let view = view.clone();
move |cx| {
_ = dock_area.update(cx, |this, cx| {
this.subscribe_panel(&view, cx);
});
}
});
}
DockItem::Panel { .. } => { DockItem::Panel { .. } => {
// Not supported // Not supported
} }
@@ -376,6 +383,8 @@ impl Render for Dock {
DockItem::Split { view, .. } => this.child(view.clone()), DockItem::Split { view, .. } => this.child(view.clone()),
DockItem::Tabs { view, .. } => this.child(view.clone()), DockItem::Tabs { view, .. } => this.child(view.clone()),
DockItem::Panel { view, .. } => this.child(view.clone().view()), DockItem::Panel { view, .. } => this.child(view.clone().view()),
// Not support to render Tiles and Tile into Dock
DockItem::Tiles { .. } => this,
}) })
.child(self.render_resize_handle(cx)) .child(self.render_resize_handle(cx))
.child(DockElement { .child(DockElement {

View File

@@ -3,18 +3,17 @@ use gpui::{
Styled as _, WindowContext, Styled as _, WindowContext,
}; };
use crate::theme::ActiveTheme as _; use super::{Panel, PanelEvent, PanelState};
use crate::theme::ActiveTheme;
use super::{DockItemState, Panel, PanelEvent};
pub(crate) struct InvalidPanel { pub(crate) struct InvalidPanel {
name: SharedString, name: SharedString,
focus_handle: FocusHandle, focus_handle: FocusHandle,
old_state: DockItemState, old_state: PanelState,
} }
impl InvalidPanel { impl InvalidPanel {
pub(crate) fn new(name: &str, state: DockItemState, cx: &mut WindowContext) -> Self { pub(crate) fn new(name: &str, state: PanelState, cx: &mut WindowContext) -> Self {
Self { Self {
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
name: SharedString::from(name.to_owned()), name: SharedString::from(name.to_owned()),
@@ -27,7 +26,7 @@ impl Panel for InvalidPanel {
"InvalidPanel" "InvalidPanel"
} }
fn dump(&self, _cx: &AppContext) -> super::DockItemState { fn dump(&self, _cx: &AppContext) -> super::PanelState {
self.old_state.clone() self.old_state.clone()
} }
} }

View File

@@ -5,9 +5,9 @@ mod panel;
mod stack_panel; mod stack_panel;
mod state; mod state;
mod tab_panel; mod tab_panel;
mod tiles;
use anyhow::Result; use anyhow::Result;
use gpui::{ use gpui::{
actions, canvas, div, prelude::FluentBuilder, AnyElement, AnyView, AppContext, Axis, Bounds, actions, canvas, div, prelude::FluentBuilder, AnyElement, AnyView, AppContext, Axis, Bounds,
Edges, Entity as _, EntityId, EventEmitter, InteractiveElement as _, IntoElement, Edges, Entity as _, EntityId, EventEmitter, InteractiveElement as _, IntoElement,
@@ -21,6 +21,7 @@ pub use panel::*;
pub use stack_panel::*; pub use stack_panel::*;
pub use state::*; pub use state::*;
pub use tab_panel::*; pub use tab_panel::*;
pub use tiles::*;
use crate::theme::ActiveTheme; use crate::theme::ActiveTheme;
@@ -74,7 +75,7 @@ pub struct DockArea {
pub enum DockItem { pub enum DockItem {
/// Split layout /// Split layout
Split { Split {
axis: gpui::Axis, axis: Axis,
items: Vec<DockItem>, items: Vec<DockItem>,
sizes: Vec<Option<Pixels>>, sizes: Vec<Option<Pixels>>,
view: View<StackPanel>, view: View<StackPanel>,
@@ -87,6 +88,11 @@ pub enum DockItem {
}, },
/// Panel layout /// Panel layout
Panel { view: Arc<dyn PanelView> }, Panel { view: Arc<dyn PanelView> },
/// Tiles layout
Tiles {
items: Vec<TileItem>,
view: View<Tiles>,
},
} }
impl DockItem { impl DockItem {
@@ -153,6 +159,51 @@ impl DockItem {
Self::Panel { view: panel } Self::Panel { view: panel }
} }
/// Create DockItem with tiles layout
///
/// This items and metas should have the same length.
pub fn tiles(
items: Vec<DockItem>,
metas: Vec<impl Into<TileMeta> + Copy>,
dock_area: &WeakView<DockArea>,
cx: &mut WindowContext,
) -> Self {
assert!(items.len() == metas.len());
let tile_panel = cx.new_view(|cx| {
let mut tiles = Tiles::new(cx);
for (ix, item) in items.clone().into_iter().enumerate() {
match item {
DockItem::Tabs { view, .. } => {
let meta: TileMeta = metas[ix].into();
let tile_item =
TileItem::new(Arc::new(view), meta.bounds).z_index(meta.z_index);
tiles.add_item(tile_item, dock_area, cx);
}
_ => {
// Ignore non-tabs items
}
}
}
tiles
});
cx.defer({
let tile_panel = tile_panel.clone();
let dock_area = dock_area.clone();
move |cx| {
_ = dock_area.update(cx, |this, cx| {
this.subscribe_panel(&tile_panel, cx);
});
}
});
Self::Tiles {
items: tile_panel.read(cx).panels.clone(),
view: tile_panel,
}
}
/// Create DockItem with tabs layout, items are displayed as tabs. /// Create DockItem with tabs layout, items are displayed as tabs.
/// ///
/// The `active_ix` is the index of the active tab, if `None` the first tab is active. /// The `active_ix` is the index of the active tab, if `None` the first tab is active.
@@ -201,10 +252,11 @@ impl DockItem {
} }
/// Returns the views of the dock item. /// Returns the views of the dock item.
fn view(&self) -> Arc<dyn PanelView> { pub fn view(&self) -> Arc<dyn PanelView> {
match self { match self {
Self::Split { view, .. } => Arc::new(view.clone()), Self::Split { view, .. } => Arc::new(view.clone()),
Self::Tabs { view, .. } => Arc::new(view.clone()), Self::Tabs { view, .. } => Arc::new(view.clone()),
Self::Tiles { view, .. } => Arc::new(view.clone()),
Self::Panel { view, .. } => view.clone(), Self::Panel { view, .. } => view.clone(),
} }
} }
@@ -217,6 +269,14 @@ impl DockItem {
} }
Self::Tabs { items, .. } => items.iter().find(|item| *item == &panel).cloned(), Self::Tabs { items, .. } => items.iter().find(|item| *item == &panel).cloned(),
Self::Panel { view } => Some(view.clone()), Self::Panel { view } => Some(view.clone()),
Self::Tiles { items, .. } => items.iter().find_map(|item| {
#[allow(clippy::op_ref)]
if &item.panel == &panel {
Some(item.panel.clone())
} else {
None
}
}),
} }
} }
@@ -252,6 +312,7 @@ impl DockItem {
stack_panel.add_panel(new_item.view(), None, dock_area.clone(), cx); stack_panel.add_panel(new_item.view(), None, dock_area.clone(), cx);
}); });
} }
Self::Tiles { .. } => {}
Self::Panel { .. } => {} Self::Panel { .. } => {}
} }
} }
@@ -269,6 +330,7 @@ impl DockItem {
item.set_collapsed(collapsed, cx); item.set_collapsed(collapsed, cx);
} }
} }
DockItem::Tiles { .. } => {}
DockItem::Panel { .. } => {} DockItem::Panel { .. } => {}
} }
} }
@@ -278,6 +340,7 @@ impl DockItem {
match self { match self {
DockItem::Tabs { view, .. } => Some(view.clone()), DockItem::Tabs { view, .. } => Some(view.clone()),
DockItem::Split { view, .. } => view.read(cx).left_top_tab_panel(true, cx), DockItem::Split { view, .. } => view.read(cx).left_top_tab_panel(true, cx),
DockItem::Tiles { .. } => None,
DockItem::Panel { .. } => None, DockItem::Panel { .. } => None,
} }
} }
@@ -287,6 +350,7 @@ impl DockItem {
match self { match self {
DockItem::Tabs { view, .. } => Some(view.clone()), DockItem::Tabs { view, .. } => Some(view.clone()),
DockItem::Split { view, .. } => view.read(cx).right_top_tab_panel(true, cx), DockItem::Split { view, .. } => view.read(cx).right_top_tab_panel(true, cx),
DockItem::Tiles { .. } => None,
DockItem::Panel { .. } => None, DockItem::Panel { .. } => None,
} }
} }
@@ -601,7 +665,7 @@ impl DockArea {
Ok(()) Ok(())
} }
/// Dump the dock panels layout to DockItemState. /// Dump the dock panels layout to PanelState.
/// ///
/// See also [DockArea::load]. /// See also [DockArea::load].
pub fn dump(&self, cx: &AppContext) -> DockAreaState { pub fn dump(&self, cx: &AppContext) -> DockAreaState {
@@ -658,6 +722,9 @@ impl DockArea {
DockItem::Tabs { .. } => { DockItem::Tabs { .. } => {
// We subscribe to the tab panel event in StackPanel's insert_panel // We subscribe to the tab panel event in StackPanel's insert_panel
} }
DockItem::Tiles { .. } => {
// We subscribe to the tab panel event in Tiles's [`add_item`](Tiles::add_item)
}
DockItem::Panel { .. } => { DockItem::Panel { .. } => {
// Not supported // Not supported
} }
@@ -727,6 +794,7 @@ impl DockArea {
match &self.items { match &self.items {
DockItem::Split { view, .. } => view.clone().into_any_element(), DockItem::Split { view, .. } => view.clone().into_any_element(),
DockItem::Tabs { view, .. } => view.clone().into_any_element(), DockItem::Tabs { view, .. } => view.clone().into_any_element(),
DockItem::Tiles { view, .. } => view.clone().into_any_element(),
DockItem::Panel { view, .. } => view.clone().view().into_any_element(), DockItem::Panel { view, .. } => view.clone().view().into_any_element(),
} }
} }
@@ -774,41 +842,50 @@ impl Render for DockArea {
if let Some(zoom_view) = self.zoom_view.clone() { if let Some(zoom_view) = self.zoom_view.clone() {
this.child(zoom_view) this.child(zoom_view)
} else { } else {
this.child( match &self.items {
div() DockItem::Tiles { view, .. } => {
.flex() // render tiles
.flex_row() this.child(view.clone())
.h_full() }
// Left dock _ => {
.when_some(self.left_dock.clone(), |this, dock| { // render dock
this.child(div().flex().flex_none().child(dock)) this.child(
.bg(cx.theme().sidebar)
.text_color(cx.theme().sidebar_foreground)
})
// Center
.child(
div() div()
.flex() .flex()
.flex_1() .flex_row()
.flex_col() .h_full()
.overflow_hidden() // Left dock
// Top center .when_some(self.left_dock.clone(), |this, dock| {
this.bg(cx.theme().sidebar)
.text_color(cx.theme().sidebar_foreground)
.child(div().flex().flex_none().child(dock))
})
// Center
.child( .child(
div() div()
.flex()
.flex_1() .flex_1()
.flex_col()
.overflow_hidden() .overflow_hidden()
.child(self.render_items(cx)), // Top center
.child(
div()
.flex_1()
.overflow_hidden()
.child(self.render_items(cx)),
)
// Bottom Dock
.when_some(self.bottom_dock.clone(), |this, dock| {
this.child(dock)
}),
) )
// Bottom Dock // Right Dock
.when_some(self.bottom_dock.clone(), |this, dock| { .when_some(self.right_dock.clone(), |this, dock| {
this.child(dock) this.child(div().flex().flex_none().child(dock))
}), }),
) )
// Right Dock }
.when_some(self.right_dock.clone(), |this, dock| { }
this.child(div().flex().flex_none().child(dock))
}),
)
} }
}) })
} }

View File

@@ -4,7 +4,7 @@ use gpui::{
}; };
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
use super::{DockArea, DockItemInfo, DockItemState}; use super::{DockArea, PanelInfo, PanelState};
use crate::{button::Button, popup_menu::PopupMenu}; use crate::{button::Button, popup_menu::PopupMenu};
pub enum PanelEvent { pub enum PanelEvent {
@@ -36,7 +36,7 @@ pub trait Panel: EventEmitter<PanelEvent> + FocusableView {
/// The title of the panel /// The title of the panel
fn title(&self, _cx: &WindowContext) -> AnyElement { fn title(&self, _cx: &WindowContext) -> AnyElement {
SharedString::from("Unnamed").into_any_element() SharedString::from("Untitled").into_any_element()
} }
/// The theme of the panel title, default is `None`. /// The theme of the panel title, default is `None`.
@@ -65,13 +65,13 @@ pub trait Panel: EventEmitter<PanelEvent> + FocusableView {
} }
/// Dump the panel, used to serialize the panel. /// Dump the panel, used to serialize the panel.
fn dump(&self, _cx: &AppContext) -> DockItemState { fn dump(&self, _cx: &AppContext) -> PanelState {
DockItemState::new(self) PanelState::new(self)
} }
} }
pub trait PanelView: 'static + Send + Sync { pub trait PanelView: 'static + Send + Sync {
fn panel_name(&self, _cx: &WindowContext) -> &'static str; fn panel_name(&self, _cx: &AppContext) -> &'static str;
fn title(&self, _cx: &WindowContext) -> AnyElement; fn title(&self, _cx: &WindowContext) -> AnyElement;
fn title_style(&self, _cx: &WindowContext) -> Option<TitleStyle>; fn title_style(&self, _cx: &WindowContext) -> Option<TitleStyle>;
fn closeable(&self, cx: &WindowContext) -> bool; fn closeable(&self, cx: &WindowContext) -> bool;
@@ -80,11 +80,11 @@ pub trait PanelView: 'static + Send + Sync {
fn toolbar_buttons(&self, cx: &WindowContext) -> Vec<Button>; fn toolbar_buttons(&self, cx: &WindowContext) -> Vec<Button>;
fn view(&self) -> AnyView; fn view(&self) -> AnyView;
fn focus_handle(&self, cx: &AppContext) -> FocusHandle; fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
fn dump(&self, cx: &AppContext) -> DockItemState; fn dump(&self, cx: &AppContext) -> PanelState;
} }
impl<T: Panel> PanelView for View<T> { impl<T: Panel> PanelView for View<T> {
fn panel_name(&self, cx: &WindowContext) -> &'static str { fn panel_name(&self, cx: &AppContext) -> &'static str {
self.read(cx).panel_name() self.read(cx).panel_name()
} }
@@ -120,7 +120,7 @@ impl<T: Panel> PanelView for View<T> {
self.read(cx).focus_handle(cx) self.read(cx).focus_handle(cx)
} }
fn dump(&self, cx: &AppContext) -> DockItemState { fn dump(&self, cx: &AppContext) -> PanelState {
self.read(cx).dump(cx) self.read(cx).dump(cx)
} }
} }
@@ -143,20 +143,20 @@ impl PartialEq for dyn PanelView {
} }
} }
type PanelRegistryItem = HashMap< type Items = HashMap<
String, String,
Arc< Arc<
dyn Fn( dyn Fn(
WeakView<DockArea>, WeakView<DockArea>,
&DockItemState, &PanelState,
&DockItemInfo, &PanelInfo,
&mut WindowContext, &mut WindowContext,
) -> Box<dyn PanelView>, ) -> Box<dyn PanelView>,
>, >,
>; >;
pub struct PanelRegistry { pub struct PanelRegistry {
pub(super) items: PanelRegistryItem, pub(super) items: Items,
} }
impl PanelRegistry { impl PanelRegistry {
@@ -178,12 +178,7 @@ impl Global for PanelRegistry {}
/// Register the Panel init by panel_name to global registry. /// Register the Panel init by panel_name to global registry.
pub fn register_panel<F>(cx: &mut AppContext, panel_name: &str, deserialize: F) pub fn register_panel<F>(cx: &mut AppContext, panel_name: &str, deserialize: F)
where where
F: Fn( F: Fn(WeakView<DockArea>, &PanelState, &PanelInfo, &mut WindowContext) -> Box<dyn PanelView>
WeakView<DockArea>,
&DockItemState,
&DockItemInfo,
&mut WindowContext,
) -> Box<dyn PanelView>
+ 'static, + 'static,
{ {
if cx.try_global::<PanelRegistry>().is_none() { if cx.try_global::<PanelRegistry>().is_none() {

View File

@@ -1,7 +1,14 @@
use gpui::{
prelude::FluentBuilder as _, AppContext, Axis, DismissEvent, EventEmitter, FocusHandle,
FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, Subscription, View,
ViewContext, VisualContext, WeakView,
};
use smallvec::SmallVec;
use std::sync::Arc; use std::sync::Arc;
use super::{DockArea, Panel, PanelEvent, PanelState, PanelView, TabPanel};
use crate::{ use crate::{
dock::DockItemInfo, dock::PanelInfo,
h_flex, h_flex,
resizable::{ resizable::{
h_resizable, resizable_panel, v_resizable, ResizablePanel, ResizablePanelEvent, h_resizable, resizable_panel, v_resizable, ResizablePanel, ResizablePanelEvent,
@@ -11,14 +18,6 @@ use crate::{
AxisExt as _, Placement, AxisExt as _, Placement,
}; };
use super::{DockArea, DockItemState, Panel, PanelEvent, PanelView, TabPanel};
use gpui::{
prelude::FluentBuilder as _, AppContext, Axis, DismissEvent, EventEmitter, FocusHandle,
FocusableView, IntoElement, ParentElement, Pixels, Render, Styled, Subscription, View,
ViewContext, VisualContext, WeakView,
};
use smallvec::SmallVec;
pub struct StackPanel { pub struct StackPanel {
pub(super) parent: Option<WeakView<StackPanel>>, pub(super) parent: Option<WeakView<StackPanel>>,
pub(super) axis: Axis, pub(super) axis: Axis,
@@ -37,12 +36,12 @@ impl Panel for StackPanel {
"StackPanel".into_any_element() "StackPanel".into_any_element()
} }
fn dump(&self, cx: &AppContext) -> DockItemState { fn dump(&self, cx: &AppContext) -> PanelState {
let sizes = self.panel_group.read(cx).sizes(); let sizes = self.panel_group.read(cx).sizes();
let mut state = DockItemState::new(self); let mut state = PanelState::new(self);
for panel in &self.panels { for panel in &self.panels {
state.add_child(panel.dump(cx)); state.add_child(panel.dump(cx));
state.info = DockItemInfo::stack(sizes.clone(), self.axis); state.info = PanelInfo::stack(sizes.clone(), self.axis);
} }
state state

View File

@@ -1,4 +1,7 @@
use gpui::{AppContext, Axis, Pixels, View, VisualContext as _, WeakView, WindowContext}; use gpui::{
point, px, size, AppContext, Axis, Bounds, Pixels, View, VisualContext as _, WeakView,
WindowContext,
};
use itertools::Itertools as _; use itertools::Itertools as _;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -14,16 +17,19 @@ pub struct DockAreaState {
/// then we can compare the version to decide whether we can use the state or ignore. /// then we can compare the version to decide whether we can use the state or ignore.
#[serde(default)] #[serde(default)]
pub version: Option<usize>, pub version: Option<usize>,
pub center: DockItemState, pub center: PanelState,
#[serde(skip_serializing_if = "Option::is_none")]
pub left_dock: Option<DockState>, pub left_dock: Option<DockState>,
#[serde(skip_serializing_if = "Option::is_none")]
pub right_dock: Option<DockState>, pub right_dock: Option<DockState>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bottom_dock: Option<DockState>, pub bottom_dock: Option<DockState>,
} }
/// Used to serialize and deserialize the Dock /// Used to serialize and deserialize the Dock
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DockState { pub struct DockState {
panel: DockItemState, panel: PanelState,
placement: DockPlacement, placement: DockPlacement,
size: Pixels, size: Pixels,
open: bool, open: bool,
@@ -59,27 +65,52 @@ impl DockState {
/// Used to serialize and deserialize the DockerItem /// Used to serialize and deserialize the DockerItem
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DockItemState { pub struct PanelState {
pub panel_name: String, pub panel_name: String,
pub children: Vec<DockItemState>, pub children: Vec<PanelState>,
pub info: DockItemInfo, pub info: PanelInfo,
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct TileMeta {
pub bounds: Bounds<Pixels>,
pub z_index: usize,
}
impl Default for TileMeta {
fn default() -> Self {
Self {
bounds: Bounds {
origin: point(px(10.), px(10.)),
size: size(px(200.), px(200.)),
},
z_index: 0,
}
}
}
impl From<Bounds<Pixels>> for TileMeta {
fn from(bounds: Bounds<Pixels>) -> Self {
Self { bounds, z_index: 0 }
}
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum DockItemInfo { pub enum PanelInfo {
#[serde(rename = "stack")] #[serde(rename = "stack")]
Stack { Stack {
sizes: Vec<Pixels>, sizes: Vec<Pixels>,
/// The axis of the stack, 0 is horizontal, 1 is vertical axis: usize, // 0 for horizontal, 1 for vertical
axis: usize,
}, },
#[serde(rename = "tabs")] #[serde(rename = "tabs")]
Tabs { active_index: usize }, Tabs { active_index: usize },
#[serde(rename = "panel")] #[serde(rename = "panel")]
Panel(serde_json::Value), Panel(serde_json::Value),
#[serde(rename = "tiles")]
Tiles { metas: Vec<TileMeta> },
} }
impl DockItemInfo { impl PanelInfo {
pub fn stack(sizes: Vec<Pixels>, axis: Axis) -> Self { pub fn stack(sizes: Vec<Pixels>, axis: Axis) -> Self {
Self::Stack { Self::Stack {
sizes, sizes,
@@ -91,8 +122,12 @@ impl DockItemInfo {
Self::Tabs { active_index } Self::Tabs { active_index }
} }
pub fn panel(value: serde_json::Value) -> Self { pub fn panel(info: serde_json::Value) -> Self {
Self::Panel(value) Self::Panel(info)
}
pub fn tiles(metas: Vec<TileMeta>) -> Self {
Self::Tiles { metas }
} }
pub fn axis(&self) -> Option<Axis> { pub fn axis(&self) -> Option<Axis> {
@@ -121,17 +156,17 @@ impl DockItemInfo {
} }
} }
impl Default for DockItemState { impl Default for PanelState {
fn default() -> Self { fn default() -> Self {
Self { Self {
panel_name: "".to_string(), panel_name: "".to_string(),
children: Vec::new(), children: Vec::new(),
info: DockItemInfo::Panel(serde_json::Value::Null), info: PanelInfo::Panel(serde_json::Value::Null),
} }
} }
} }
impl DockItemState { impl PanelState {
pub fn new<P: Panel>(panel: &P) -> Self { pub fn new<P: Panel>(panel: &P) -> Self {
Self { Self {
panel_name: panel.panel_name().to_string(), panel_name: panel.panel_name().to_string(),
@@ -139,7 +174,7 @@ impl DockItemState {
} }
} }
pub fn add_child(&mut self, panel: DockItemState) { pub fn add_child(&mut self, panel: PanelState) {
self.children.push(panel); self.children.push(panel);
} }
@@ -153,7 +188,7 @@ impl DockItemState {
.collect(); .collect();
match info { match info {
DockItemInfo::Stack { sizes, axis } => { PanelInfo::Stack { sizes, axis } => {
let axis = if axis == 0 { let axis = if axis == 0 {
Axis::Horizontal Axis::Horizontal
} else { } else {
@@ -162,7 +197,7 @@ impl DockItemState {
let sizes = sizes.iter().map(|s| Some(*s)).collect_vec(); let sizes = sizes.iter().map(|s| Some(*s)).collect_vec();
DockItem::split_with_sizes(axis, items, sizes, &dock_area, cx) DockItem::split_with_sizes(axis, items, sizes, &dock_area, cx)
} }
DockItemInfo::Tabs { active_index } => { PanelInfo::Tabs { active_index } => {
if items.len() == 1 { if items.len() == 1 {
return items[0].clone(); return items[0].clone();
} }
@@ -172,14 +207,15 @@ impl DockItemState {
.flat_map(|item| match item { .flat_map(|item| match item {
DockItem::Tabs { items, .. } => items.clone(), DockItem::Tabs { items, .. } => items.clone(),
_ => { _ => {
unreachable!("Invalid DockItem type in DockItemInfo::Tabs") // ignore invalid panels in tabs
vec![]
} }
}) })
.collect_vec(); .collect_vec();
DockItem::tabs(items, Some(active_index), &dock_area, cx) DockItem::tabs(items, Some(active_index), &dock_area, cx)
} }
DockItemInfo::Panel(_) => { PanelInfo::Panel(_) => {
let view = if let Some(f) = cx let view = if let Some(f) = cx
.global::<PanelRegistry>() .global::<PanelRegistry>()
.items .items
@@ -196,6 +232,7 @@ impl DockItemState {
DockItem::tabs(vec![view.into()], None, &dock_area, cx) DockItem::tabs(vec![view.into()], None, &dock_area, cx)
} }
PanelInfo::Tiles { metas } => DockItem::tiles(items, metas, &dock_area, cx),
} }
} }
} }

View File

@@ -8,12 +8,12 @@ use gpui::{
use std::sync::Arc; use std::sync::Arc;
use super::{ use super::{
ClosePanel, DockArea, DockItemState, DockPlacement, Panel, PanelEvent, PanelStyle, PanelView, ClosePanel, DockArea, DockPlacement, Panel, PanelEvent, PanelState, PanelStyle, PanelView,
StackPanel, ToggleZoom, StackPanel, ToggleZoom,
}; };
use crate::{ use crate::{
button::{Button, ButtonVariants as _}, button::{Button, ButtonVariants as _},
dock::DockItemInfo, dock::PanelInfo,
h_flex, h_flex,
popup_menu::{PopupMenu, PopupMenuExt}, popup_menu::{PopupMenu, PopupMenuExt},
tab::{Tab, TabBar}, tab::{Tab, TabBar},
@@ -122,11 +122,11 @@ impl Panel for TabPanel {
} }
} }
fn dump(&self, cx: &AppContext) -> DockItemState { fn dump(&self, cx: &AppContext) -> PanelState {
let mut state = DockItemState::new(self); let mut state = PanelState::new(self);
for panel in self.panels.iter() { for panel in self.panels.iter() {
state.add_child(panel.dump(cx)); state.add_child(panel.dump(cx));
state.info = DockItemInfo::tabs(self.active_ix); state.info = PanelInfo::tabs(self.active_ix);
} }
state state
} }
@@ -720,13 +720,11 @@ impl TabPanel {
// If target is same tab, and it is only one panel, do nothing. // If target is same tab, and it is only one panel, do nothing.
if is_same_tab && ix.is_none() { if is_same_tab && ix.is_none() {
#[allow(clippy::if_same_then_else)]
if self.will_split_placement.is_none() { if self.will_split_placement.is_none() {
return; return;
} else { } else if self.panels.len() == 1 {
#[allow(clippy::collapsible_else_if)] return;
if self.panels.len() == 1 {
return;
}
} }
} }
@@ -888,8 +886,11 @@ impl FocusableView for TabPanel {
} }
} }
} }
impl EventEmitter<DismissEvent> for TabPanel {} impl EventEmitter<DismissEvent> for TabPanel {}
impl EventEmitter<PanelEvent> for TabPanel {} impl EventEmitter<PanelEvent> for TabPanel {}
impl Render for TabPanel { impl Render for TabPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl gpui::IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl gpui::IntoElement {
let focus_handle = self.focus_handle(cx); let focus_handle = self.focus_handle(cx);

708
crates/ui/src/dock/tiles.rs Normal file
View File

@@ -0,0 +1,708 @@
use gpui::{
canvas, div, point, px, size, AnyElement, AppContext, Bounds, DismissEvent, DragMoveEvent,
Entity, EntityId, EventEmitter, FocusHandle, FocusableView, Half, InteractiveElement,
IntoElement, MouseButton, MouseDownEvent, MouseUpEvent, ParentElement, Pixels, Point, Render,
ScrollHandle, Size, StatefulInteractiveElement, Styled, ViewContext, VisualContext, WeakView,
WindowContext,
};
use std::{
cell::Cell,
fmt::{Debug, Formatter},
rc::Rc,
sync::Arc,
};
use super::{DockArea, Panel, PanelEvent, PanelInfo, PanelState, PanelView, TabPanel, TileMeta};
use crate::{
h_flex,
scroll::{Scrollbar, ScrollbarState},
theme::ActiveTheme,
v_flex, Icon, IconName,
};
const MINIMUM_SIZE: Size<Pixels> = size(px(100.), px(100.));
const DRAG_BAR_HEIGHT: Pixels = px(30.);
const HANDLE_SIZE: Pixels = px(20.0);
#[derive(Clone, Render)]
pub struct DragMoving(EntityId);
#[derive(Clone, Render)]
pub struct DragResizing(EntityId);
#[derive(Clone)]
struct ResizeDrag {
axis: ResizeAxis,
last_position: Point<Pixels>,
last_bounds: Bounds<Pixels>,
}
#[derive(Clone, PartialEq)]
enum ResizeAxis {
Horizontal,
Vertical,
Both,
}
/// TileItem is a moveable and resizable panel that can be added to a Tiles view.
#[derive(Clone)]
pub struct TileItem {
pub(crate) panel: Arc<dyn PanelView>,
bounds: Bounds<Pixels>,
z_index: usize,
}
impl Debug for TileItem {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TileItem")
.field("bounds", &self.bounds)
.field("z_index", &self.z_index)
.finish()
}
}
impl TileItem {
pub fn new(panel: Arc<dyn PanelView>, bounds: Bounds<Pixels>) -> Self {
Self {
panel,
bounds,
z_index: 0,
}
}
pub fn z_index(mut self, z_index: usize) -> Self {
self.z_index = z_index;
self
}
}
/// Tiles is a canvas that can contain multiple panels, each of which can be dragged and resized.
pub struct Tiles {
focus_handle: FocusHandle,
pub(crate) panels: Vec<TileItem>,
dragging_index: Option<usize>,
dragging_initial_mouse: Point<Pixels>,
dragging_initial_bounds: Bounds<Pixels>,
resizing_index: Option<usize>,
resizing_drag_data: Option<ResizeDrag>,
bounds: Bounds<Pixels>,
scroll_state: Rc<Cell<ScrollbarState>>,
scroll_handle: ScrollHandle,
}
impl Panel for Tiles {
fn panel_name(&self) -> &'static str {
"Tiles"
}
fn title(&self, _cx: &WindowContext) -> AnyElement {
"Tiles".into_any_element()
}
fn dump(&self, cx: &AppContext) -> PanelState {
let panels = self
.panels
.iter()
.map(|item: &TileItem| item.panel.dump(cx))
.collect();
let metas = self
.panels
.iter()
.map(|item: &TileItem| TileMeta {
bounds: item.bounds,
z_index: item.z_index,
})
.collect();
let mut state = PanelState::new(self);
state.panel_name = self.panel_name().to_string();
state.children = panels;
state.info = PanelInfo::Tiles { metas };
state
}
}
impl Tiles {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
Self {
focus_handle: cx.focus_handle(),
panels: vec![],
dragging_index: None,
dragging_initial_mouse: Point::default(),
dragging_initial_bounds: Bounds::default(),
resizing_index: None,
resizing_drag_data: None,
bounds: Bounds::default(),
scroll_state: Rc::new(Cell::new(ScrollbarState::default())),
scroll_handle: ScrollHandle::default(),
}
}
fn sorted_panels(&self) -> Vec<TileItem> {
let mut items: Vec<(usize, TileItem)> = self.panels.iter().cloned().enumerate().collect();
items.sort_by(|a, b| a.1.z_index.cmp(&b.1.z_index).then_with(|| a.0.cmp(&b.0)));
items.into_iter().map(|(_, item)| item).collect()
}
/// Return the index of the panel.
#[inline]
pub(crate) fn index_of(&self, panel: Arc<dyn PanelView>) -> Option<usize> {
#[allow(clippy::op_ref)]
self.panels.iter().position(|p| &p.panel == &panel)
}
/// Remove panel from the children.
pub fn remove(&mut self, panel: Arc<dyn PanelView>, cx: &mut ViewContext<Self>) {
if let Some(ix) = self.index_of(panel.clone()) {
self.panels.remove(ix);
cx.emit(PanelEvent::LayoutChanged);
}
}
fn update_initial_position(&mut self, position: Point<Pixels>, cx: &mut ViewContext<'_, Self>) {
let Some((index, item)) = self.find_at_position(position) else {
return;
};
let inner_pos = position - self.bounds.origin;
let bounds = item.bounds;
self.dragging_index = Some(index);
self.dragging_initial_mouse = inner_pos;
self.dragging_initial_bounds = bounds;
cx.notify();
}
fn update_position(&mut self, pos: Point<Pixels>, cx: &mut ViewContext<'_, Self>) {
let Some(index) = self.dragging_index else {
return;
};
let Some(item) = self.panels.get_mut(index) else {
return;
};
let adjusted_position = pos - self.bounds.origin;
let delta = adjusted_position - self.dragging_initial_mouse;
let mut new_origin = self.dragging_initial_bounds.origin + delta;
new_origin.x = new_origin.x.max(px(0.0));
new_origin.y = new_origin.y.max(px(0.0));
item.bounds.origin = round_point_to_nearest_ten(new_origin);
cx.notify();
}
fn update_resizing_drag(&mut self, drag_data: ResizeDrag, cx: &mut ViewContext<'_, Self>) {
if let Some((index, _item)) = self.find_at_position(drag_data.last_position) {
self.resizing_index = Some(index);
self.resizing_drag_data = Some(drag_data);
cx.notify();
}
}
fn resize_width(&mut self, new_width: Pixels, cx: &mut ViewContext<'_, Self>) {
if let Some(index) = self.resizing_index {
if let Some(item) = self.panels.get_mut(index) {
item.bounds.size.width = round_to_nearest_ten(new_width);
cx.notify();
}
}
}
fn resize_height(&mut self, new_height: Pixels, cx: &mut ViewContext<'_, Self>) {
if let Some(index) = self.resizing_index {
if let Some(item) = self.panels.get_mut(index) {
item.bounds.size.height = round_to_nearest_ten(new_height);
cx.notify();
}
}
}
pub fn add_item(
&mut self,
item: TileItem,
dock_area: &WeakView<DockArea>,
cx: &mut ViewContext<Self>,
) {
self.panels.push(item.clone());
cx.window_context().defer({
let panel = item.panel.clone();
let dock_area = dock_area.clone();
move |cx| {
// Subscribe to the panel's layout change event.
_ = dock_area.update(cx, |this, cx| {
if let Ok(tab_panel) = panel.view().downcast::<TabPanel>() {
this.subscribe_panel(&tab_panel, cx);
}
});
}
});
cx.emit(PanelEvent::LayoutChanged);
cx.notify();
}
/// Find the panel at a given position, considering z-index
fn find_at_position(&self, position: Point<Pixels>) -> Option<(usize, &TileItem)> {
let inner_pos = position - self.bounds.origin;
let mut panels_with_indices: Vec<(usize, &TileItem)> =
self.panels.iter().enumerate().collect();
panels_with_indices
.sort_by(|a, b| b.1.z_index.cmp(&a.1.z_index).then_with(|| b.0.cmp(&a.0)));
for (index, item) in panels_with_indices {
let extended_bounds = Bounds::new(
item.bounds.origin,
item.bounds.size + size(HANDLE_SIZE, HANDLE_SIZE) / 2.0,
);
if extended_bounds.contains(&inner_pos) {
return Some((index, item));
}
}
None
}
#[inline]
fn reset_current_index(&mut self) {
self.dragging_index = None;
self.resizing_index = None;
}
/// Bring the panel of target_index to front, returns (old_index, new_index) if successful
fn bring_to_front(&mut self, target_index: Option<usize>) -> Option<(usize, usize)> {
if let Some(old_index) = target_index {
if old_index < self.panels.len() {
let item = self.panels.remove(old_index);
self.panels.push(item);
let new_index = self.panels.len() - 1;
self.reset_current_index();
return Some((old_index, new_index));
}
}
None
}
/// Produce a vector of AnyElement representing the three possible resize handles
fn render_resize_handles(
&mut self,
cx: &mut ViewContext<Self>,
entity_id: EntityId,
item: &TileItem,
is_occluded: impl Fn(&Bounds<Pixels>) -> bool,
) -> Vec<AnyElement> {
let panel_bounds = item.bounds;
let right_handle_bounds = Bounds::new(
panel_bounds.origin + point(panel_bounds.size.width - HANDLE_SIZE.half(), px(0.0)),
size(HANDLE_SIZE.half(), panel_bounds.size.height),
);
let bottom_handle_bounds = Bounds::new(
panel_bounds.origin + point(px(0.0), panel_bounds.size.height - HANDLE_SIZE.half()),
size(panel_bounds.size.width, HANDLE_SIZE.half()),
);
let corner_handle_bounds = Bounds::new(
panel_bounds.origin
+ point(
panel_bounds.size.width - HANDLE_SIZE.half(),
panel_bounds.size.height - HANDLE_SIZE.half(),
),
size(HANDLE_SIZE.half(), HANDLE_SIZE.half()),
);
let mut elements = Vec::new();
// Right resize handle
elements.push(if !is_occluded(&right_handle_bounds) {
div()
.id("right-resize-handle")
.cursor_col_resize()
.absolute()
.top(px(0.0))
.right(-HANDLE_SIZE.half())
.w(HANDLE_SIZE)
.h(panel_bounds.size.height)
.on_mouse_down(
MouseButton::Left,
cx.listener({
move |this, event: &MouseDownEvent, cx| {
let last_position = event.position;
let drag_data = ResizeDrag {
axis: ResizeAxis::Horizontal,
last_position,
last_bounds: panel_bounds,
};
this.update_resizing_drag(drag_data, cx);
if let Some((_, new_ix)) = this.bring_to_front(this.resizing_index) {
this.resizing_index = Some(new_ix);
}
}
}),
)
.on_drag(DragResizing(entity_id), |drag, _, cx| {
cx.stop_propagation();
cx.new_view(|_| drag.clone())
})
.on_drag_move(
cx.listener(move |this, e: &DragMoveEvent<DragResizing>, cx| {
match e.drag(cx) {
DragResizing(id) => {
if *id != entity_id {
return;
}
if let Some(ref drag_data) = this.resizing_drag_data {
if drag_data.axis != ResizeAxis::Horizontal {
return;
}
let pos = e.event.position;
let delta = pos.x - drag_data.last_position.x;
let new_width = (drag_data.last_bounds.size.width + delta)
.max(MINIMUM_SIZE.width);
this.resize_width(new_width, cx);
}
}
}
}),
)
.into_any_element()
} else {
div().into_any_element()
});
// Bottom resize handle
elements.push(if !is_occluded(&bottom_handle_bounds) {
div()
.id("bottom-resize-handle")
.cursor_row_resize()
.absolute()
.left(px(0.0))
.bottom(-HANDLE_SIZE.half())
.w(panel_bounds.size.width)
.h(HANDLE_SIZE)
.on_mouse_down(
MouseButton::Left,
cx.listener({
move |this, event: &MouseDownEvent, cx| {
let last_position = event.position;
let drag_data = ResizeDrag {
axis: ResizeAxis::Vertical,
last_position,
last_bounds: panel_bounds,
};
this.update_resizing_drag(drag_data, cx);
if let Some((_, new_ix)) = this.bring_to_front(this.resizing_index) {
this.resizing_index = Some(new_ix);
}
}
}),
)
.on_drag(DragResizing(entity_id), |drag, _, cx| {
cx.stop_propagation();
cx.new_view(|_| drag.clone())
})
.on_drag_move(
cx.listener(move |this, e: &DragMoveEvent<DragResizing>, cx| {
match e.drag(cx) {
DragResizing(id) => {
if *id != entity_id {
return;
}
if let Some(ref drag_data) = this.resizing_drag_data {
let pos = e.event.position;
let delta = pos.y - drag_data.last_position.y;
let new_height = (drag_data.last_bounds.size.height + delta)
.max(MINIMUM_SIZE.width);
this.resize_height(new_height, cx);
}
}
}
}),
)
.into_any_element()
} else {
div().into_any_element()
});
// Corner resize handle
elements.push(if !is_occluded(&corner_handle_bounds) {
div()
.id("corner-resize-handle")
.cursor_nwse_resize()
.absolute()
.right(-HANDLE_SIZE.half())
.bottom(-HANDLE_SIZE.half())
.w(HANDLE_SIZE)
.h(HANDLE_SIZE)
.child(
Icon::new(IconName::ResizeCorner)
.size(HANDLE_SIZE.half())
.text_color(cx.theme().foreground.opacity(0.3)),
)
.on_mouse_down(
MouseButton::Left,
cx.listener({
move |this, event: &MouseDownEvent, cx| {
let last_position = event.position;
let drag_data = ResizeDrag {
axis: ResizeAxis::Both,
last_position,
last_bounds: panel_bounds,
};
this.update_resizing_drag(drag_data, cx);
if let Some((_, new_ix)) = this.bring_to_front(this.resizing_index) {
this.resizing_index = Some(new_ix);
}
}
}),
)
.on_drag(DragResizing(entity_id), |drag, _, cx| {
cx.stop_propagation();
cx.new_view(|_| drag.clone())
})
.on_drag_move(
cx.listener(move |this, e: &DragMoveEvent<DragResizing>, cx| {
match e.drag(cx) {
DragResizing(id) => {
if *id != entity_id {
return;
}
if let Some(ref drag_data) = this.resizing_drag_data {
if drag_data.axis != ResizeAxis::Both {
return;
}
let pos = e.event.position;
let delta_x = pos.x - drag_data.last_position.x;
let delta_y = pos.y - drag_data.last_position.y;
let new_width = (drag_data.last_bounds.size.width + delta_x)
.max(MINIMUM_SIZE.width);
let new_height = (drag_data.last_bounds.size.height + delta_y)
.max(MINIMUM_SIZE.height);
this.resize_height(new_height, cx);
this.resize_width(new_width, cx);
}
}
}
}),
)
.into_any_element()
} else {
div().into_any_element()
});
elements
}
/// Produce the drag-bar element for the given panel item
fn render_drag_bar(
&mut self,
cx: &mut ViewContext<Self>,
entity_id: EntityId,
item: &TileItem,
is_occluded: &impl Fn(&Bounds<Pixels>) -> bool,
) -> AnyElement {
let drag_bar_bounds = Bounds::new(
item.bounds.origin,
Size {
width: item.bounds.size.width,
height: DRAG_BAR_HEIGHT,
},
);
if !is_occluded(&drag_bar_bounds) {
h_flex()
.id("drag-bar")
.cursor_grab()
.absolute()
.w_full()
.h(DRAG_BAR_HEIGHT)
.bg(cx.theme().transparent)
.on_mouse_down(
MouseButton::Left,
cx.listener(move |this, event: &MouseDownEvent, cx| {
let last_position = event.position;
this.update_initial_position(last_position, cx);
if let Some((_, new_ix)) = this.bring_to_front(this.dragging_index) {
this.dragging_index = Some(new_ix);
}
}),
)
.on_drag(DragMoving(entity_id), |drag, _, cx| {
cx.stop_propagation();
cx.new_view(|_| drag.clone())
})
.on_drag_move(cx.listener(move |this, e: &DragMoveEvent<DragMoving>, cx| {
match e.drag(cx) {
DragMoving(id) => {
if *id != entity_id {
return;
}
this.update_position(e.event.position, cx);
}
}
}))
.into_any_element()
} else {
div().into_any_element()
}
}
fn render_panel(
&mut self,
item: &TileItem,
ix: usize,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let entity_id = cx.entity_id();
let panel_view = item.panel.view();
let is_occluded = {
let panels = self.panels.clone();
move |bounds: &Bounds<Pixels>| {
let this_z = panels[ix].z_index;
let this_ix = ix;
panels.iter().enumerate().any(|(sub_ix, other_item)| {
if sub_ix == this_ix {
return false;
}
let other_is_above = (other_item.z_index > this_z)
|| (other_item.z_index == this_z && sub_ix > this_ix);
other_is_above && other_item.bounds.intersects(bounds)
})
}
};
v_flex()
.bg(cx.theme().background)
.border_1()
.border_color(cx.theme().border)
.absolute()
.left(item.bounds.origin.x - px(1.))
.top(item.bounds.origin.y - px(1.))
.w(item.bounds.size.width + px(1.))
.h(item.bounds.size.height + px(1.))
.child(
h_flex()
.w_full()
.h_full()
.overflow_hidden()
.child(panel_view),
)
.children(self.render_resize_handles(cx, entity_id, item, &is_occluded))
.child(self.render_drag_bar(cx, entity_id, item, &is_occluded))
}
}
#[inline]
fn round_to_nearest_ten(value: Pixels) -> Pixels {
px((value.0 / 10.0).round() * 10.0)
}
#[inline]
fn round_point_to_nearest_ten(point: Point<Pixels>) -> Point<Pixels> {
Point::new(round_to_nearest_ten(point.x), round_to_nearest_ten(point.y))
}
impl FocusableView for Tiles {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl EventEmitter<PanelEvent> for Tiles {}
impl EventEmitter<DismissEvent> for Tiles {}
impl Render for Tiles {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let view = cx.view().clone();
let view_id = view.entity_id();
let panels = self.sorted_panels();
let scroll_bounds =
self.panels
.iter()
.fold(Bounds::default(), |acc: Bounds<Pixels>, item| Bounds {
origin: Point {
x: acc.origin.x.min(item.bounds.origin.x),
y: acc.origin.y.min(item.bounds.origin.y),
},
size: Size {
width: acc.size.width.max(item.bounds.right()),
height: acc.size.height.max(item.bounds.bottom()),
},
});
let scroll_size = scroll_bounds.size - size(scroll_bounds.origin.x, scroll_bounds.origin.y);
div()
.relative()
.bg(cx.theme().background)
.child(
div()
.id("tiles")
.track_scroll(&self.scroll_handle)
.size_full()
.overflow_scroll()
.children(
panels
.into_iter()
.enumerate()
.map(|(ix, item)| self.render_panel(&item, ix, cx)),
)
.child({
canvas(
move |bounds, cx| view.update(cx, |r, _| r.bounds = bounds),
|_, _, _| {},
)
.absolute()
.size_full()
}),
)
.on_mouse_up(
MouseButton::Left,
cx.listener(move |this, _event: &MouseUpEvent, cx| {
if this.dragging_index.is_some()
|| this.resizing_index.is_some()
|| this.resizing_drag_data.is_some()
{
this.reset_current_index();
this.resizing_drag_data = None;
cx.emit(PanelEvent::LayoutChanged);
cx.notify();
}
}),
)
.on_mouse_down(
MouseButton::Left,
cx.listener(move |this, event: &MouseDownEvent, cx| {
if this.resizing_index.is_none() && this.dragging_index.is_none() {
let position = event.position;
if let Some((index, _)) = this.find_at_position(position) {
this.bring_to_front(Some(index));
cx.notify();
}
}
}),
)
.child(
div()
.absolute()
.top_0()
.left_0()
.right_0()
.bottom_0()
.child(Scrollbar::both(
view_id,
self.scroll_state.clone(),
self.scroll_handle.clone(),
scroll_size,
)),
)
.size_full()
}
}

View File

@@ -75,6 +75,7 @@ pub enum IconName {
ThumbsUp, ThumbsUp,
TriangleAlert, TriangleAlert,
Upload, Upload,
ResizeCorner,
WindowClose, WindowClose,
WindowMaximize, WindowMaximize,
WindowMinimize, WindowMinimize,
@@ -84,79 +85,80 @@ pub enum IconName {
impl IconName { impl IconName {
pub fn path(self) -> SharedString { pub fn path(self) -> SharedString {
match self { match self {
IconName::ALargeSmall => "icons/a-large-small.svg", Self::ALargeSmall => "icons/a-large-small.svg",
IconName::ArrowDown => "icons/arrow-down.svg", Self::ArrowDown => "icons/arrow-down.svg",
IconName::ArrowLeft => "icons/arrow-left.svg", Self::ArrowLeft => "icons/arrow-left.svg",
IconName::ArrowRight => "icons/arrow-right.svg", Self::ArrowRight => "icons/arrow-right.svg",
IconName::ArrowUp => "icons/arrow-up.svg", Self::ArrowUp => "icons/arrow-up.svg",
IconName::ArrowUpCircle => "icons/arrow-up-circle.svg", Self::ArrowUpCircle => "icons/arrow-up-circle.svg",
IconName::Asterisk => "icons/asterisk.svg", Self::Asterisk => "icons/asterisk.svg",
IconName::Bell => "icons/bell.svg", Self::Bell => "icons/bell.svg",
IconName::BookOpen => "icons/book-open.svg", Self::BookOpen => "icons/book-open.svg",
IconName::Bot => "icons/bot.svg", Self::Bot => "icons/bot.svg",
IconName::Calendar => "icons/calendar.svg", Self::Calendar => "icons/calendar.svg",
IconName::ChartPie => "icons/chart-pie.svg", Self::ChartPie => "icons/chart-pie.svg",
IconName::Check => "icons/check.svg", Self::Check => "icons/check.svg",
IconName::ChevronDown => "icons/chevron-down.svg", Self::ChevronDown => "icons/chevron-down.svg",
IconName::ChevronLeft => "icons/chevron-left.svg", Self::ChevronLeft => "icons/chevron-left.svg",
IconName::ChevronRight => "icons/chevron-right.svg", Self::ChevronRight => "icons/chevron-right.svg",
IconName::ChevronUp => "icons/chevron-up.svg", Self::ChevronUp => "icons/chevron-up.svg",
IconName::ChevronsUpDown => "icons/chevrons-up-down.svg", Self::ChevronsUpDown => "icons/chevrons-up-down.svg",
IconName::CircleCheck => "icons/circle-check.svg", Self::CircleCheck => "icons/circle-check.svg",
IconName::CircleUser => "icons/circle-user.svg", Self::CircleUser => "icons/circle-user.svg",
IconName::CircleX => "icons/circle-x.svg", Self::CircleX => "icons/circle-x.svg",
IconName::Close => "icons/close.svg", Self::Close => "icons/close.svg",
IconName::Copy => "icons/copy.svg", Self::Copy => "icons/copy.svg",
IconName::Dash => "icons/dash.svg", Self::Dash => "icons/dash.svg",
IconName::Delete => "icons/delete.svg", Self::Delete => "icons/delete.svg",
IconName::Ellipsis => "icons/ellipsis.svg", Self::Ellipsis => "icons/ellipsis.svg",
IconName::EllipsisVertical => "icons/ellipsis-vertical.svg", Self::EllipsisVertical => "icons/ellipsis-vertical.svg",
IconName::Eye => "icons/eye.svg", Self::Eye => "icons/eye.svg",
IconName::EyeOff => "icons/eye-off.svg", Self::EyeOff => "icons/eye-off.svg",
IconName::Frame => "icons/frame.svg", Self::Frame => "icons/frame.svg",
IconName::GalleryVerticalEnd => "icons/gallery-vertical-end.svg", Self::GalleryVerticalEnd => "icons/gallery-vertical-end.svg",
IconName::GitHub => "icons/github.svg", Self::GitHub => "icons/github.svg",
IconName::Globe => "icons/globe.svg", Self::Globe => "icons/globe.svg",
IconName::Heart => "icons/heart.svg", Self::Heart => "icons/heart.svg",
IconName::HeartOff => "icons/heart-off.svg", Self::HeartOff => "icons/heart-off.svg",
IconName::Inbox => "icons/inbox.svg", Self::Inbox => "icons/inbox.svg",
IconName::Info => "icons/info.svg", Self::Info => "icons/info.svg",
IconName::LayoutDashboard => "icons/layout-dashboard.svg", Self::LayoutDashboard => "icons/layout-dashboard.svg",
IconName::Loader => "icons/loader.svg", Self::Loader => "icons/loader.svg",
IconName::LoaderCircle => "icons/loader-circle.svg", Self::LoaderCircle => "icons/loader-circle.svg",
IconName::Map => "icons/map.svg", Self::Map => "icons/map.svg",
IconName::Maximize => "icons/maximize.svg", Self::Maximize => "icons/maximize.svg",
IconName::Menu => "icons/menu.svg", Self::Menu => "icons/menu.svg",
IconName::Minimize => "icons/minimize.svg", Self::Minimize => "icons/minimize.svg",
IconName::Minus => "icons/minus.svg", Self::Minus => "icons/minus.svg",
IconName::Moon => "icons/moon.svg", Self::Moon => "icons/moon.svg",
IconName::Palette => "icons/palette.svg", Self::Palette => "icons/palette.svg",
IconName::PanelBottom => "icons/panel-bottom.svg", Self::PanelBottom => "icons/panel-bottom.svg",
IconName::PanelBottomOpen => "icons/panel-bottom-open.svg", Self::PanelBottomOpen => "icons/panel-bottom-open.svg",
IconName::PanelLeft => "icons/panel-left.svg", Self::PanelLeft => "icons/panel-left.svg",
IconName::PanelLeftClose => "icons/panel-left-close.svg", Self::PanelLeftClose => "icons/panel-left-close.svg",
IconName::PanelLeftOpen => "icons/panel-left-open.svg", Self::PanelLeftOpen => "icons/panel-left-open.svg",
IconName::PanelRight => "icons/panel-right.svg", Self::PanelRight => "icons/panel-right.svg",
IconName::PanelRightClose => "icons/panel-right-close.svg", Self::PanelRightClose => "icons/panel-right-close.svg",
IconName::PanelRightOpen => "icons/panel-right-open.svg", Self::PanelRightOpen => "icons/panel-right-open.svg",
IconName::Plus => "icons/plus.svg", Self::Plus => "icons/plus.svg",
IconName::Search => "icons/search.svg", Self::Search => "icons/search.svg",
IconName::Settings => "icons/settings.svg", Self::Settings => "icons/settings.svg",
IconName::Settings2 => "icons/settings-2.svg", Self::Settings2 => "icons/settings-2.svg",
IconName::SortAscending => "icons/sort-ascending.svg", Self::SortAscending => "icons/sort-ascending.svg",
IconName::SortDescending => "icons/sort-descending.svg", Self::SortDescending => "icons/sort-descending.svg",
IconName::SquareTerminal => "icons/square-terminal.svg", Self::SquareTerminal => "icons/square-terminal.svg",
IconName::Star => "icons/star.svg", Self::Star => "icons/star.svg",
IconName::StarOff => "icons/star-off.svg", Self::StarOff => "icons/star-off.svg",
IconName::Sun => "icons/sun.svg", Self::Sun => "icons/sun.svg",
IconName::ThumbsDown => "icons/thumbs-down.svg", Self::ThumbsDown => "icons/thumbs-down.svg",
IconName::ThumbsUp => "icons/thumbs-up.svg", Self::ThumbsUp => "icons/thumbs-up.svg",
IconName::TriangleAlert => "icons/triangle-alert.svg", Self::TriangleAlert => "icons/triangle-alert.svg",
IconName::Upload => "icons/upload.svg", Self::Upload => "icons/upload.svg",
IconName::WindowClose => "icons/window-close.svg", Self::ResizeCorner => "icons/resize-corner.svg",
IconName::WindowMaximize => "icons/window-maximize.svg", Self::WindowClose => "icons/window-close.svg",
IconName::WindowMinimize => "icons/window-minimize.svg", Self::WindowMaximize => "icons/window-maximize.svg",
IconName::WindowRestore => "icons/window-restore.svg", Self::WindowMinimize => "icons/window-minimize.svg",
Self::WindowRestore => "icons/window-restore.svg",
} }
.into() .into()
} }