feat: add tiles from gpui-components
This commit is contained in:
9
assets/icons/resize-corner.svg
Normal file
9
assets/icons/resize-corner.svg
Normal 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 |
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,6 +842,13 @@ 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 {
|
||||||
|
match &self.items {
|
||||||
|
DockItem::Tiles { view, .. } => {
|
||||||
|
// render tiles
|
||||||
|
this.child(view.clone())
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
// render dock
|
||||||
this.child(
|
this.child(
|
||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
@@ -781,9 +856,9 @@ impl Render for DockArea {
|
|||||||
.h_full()
|
.h_full()
|
||||||
// Left dock
|
// Left dock
|
||||||
.when_some(self.left_dock.clone(), |this, dock| {
|
.when_some(self.left_dock.clone(), |this, dock| {
|
||||||
this.child(div().flex().flex_none().child(dock))
|
this.bg(cx.theme().sidebar)
|
||||||
.bg(cx.theme().sidebar)
|
|
||||||
.text_color(cx.theme().sidebar_foreground)
|
.text_color(cx.theme().sidebar_foreground)
|
||||||
|
.child(div().flex().flex_none().child(dock))
|
||||||
})
|
})
|
||||||
// Center
|
// Center
|
||||||
.child(
|
.child(
|
||||||
@@ -810,6 +885,8 @@ impl Render for DockArea {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,15 +720,13 @@ 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)]
|
|
||||||
if self.panels.len() == 1 {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Here is looks like remove_panel on a same item, but it difference.
|
// Here is looks like remove_panel on a same item, but it difference.
|
||||||
//
|
//
|
||||||
@@ -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
708
crates/ui/src/dock/tiles.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user