feat: add tiles from gpui-components
This commit is contained in:
@@ -1,7 +1,3 @@
|
||||
//! Dock is a fixed container that places at left, bottom, right of the Windows.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder as _, px, Axis, Element, Entity, InteractiveElement as _,
|
||||
IntoElement, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, Render,
|
||||
@@ -9,6 +5,7 @@ use gpui::{
|
||||
WeakView, WindowContext,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
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 { .. } => {
|
||||
// Not supported
|
||||
}
|
||||
@@ -376,6 +383,8 @@ impl Render for Dock {
|
||||
DockItem::Split { view, .. } => this.child(view.clone()),
|
||||
DockItem::Tabs { view, .. } => this.child(view.clone()),
|
||||
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(DockElement {
|
||||
|
||||
@@ -3,18 +3,17 @@ use gpui::{
|
||||
Styled as _, WindowContext,
|
||||
};
|
||||
|
||||
use crate::theme::ActiveTheme as _;
|
||||
|
||||
use super::{DockItemState, Panel, PanelEvent};
|
||||
use super::{Panel, PanelEvent, PanelState};
|
||||
use crate::theme::ActiveTheme;
|
||||
|
||||
pub(crate) struct InvalidPanel {
|
||||
name: SharedString,
|
||||
focus_handle: FocusHandle,
|
||||
old_state: DockItemState,
|
||||
old_state: PanelState,
|
||||
}
|
||||
|
||||
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 {
|
||||
focus_handle: cx.focus_handle(),
|
||||
name: SharedString::from(name.to_owned()),
|
||||
@@ -27,7 +26,7 @@ impl Panel for InvalidPanel {
|
||||
"InvalidPanel"
|
||||
}
|
||||
|
||||
fn dump(&self, _cx: &AppContext) -> super::DockItemState {
|
||||
fn dump(&self, _cx: &AppContext) -> super::PanelState {
|
||||
self.old_state.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ mod panel;
|
||||
mod stack_panel;
|
||||
mod state;
|
||||
mod tab_panel;
|
||||
mod tiles;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use gpui::{
|
||||
actions, canvas, div, prelude::FluentBuilder, AnyElement, AnyView, AppContext, Axis, Bounds,
|
||||
Edges, Entity as _, EntityId, EventEmitter, InteractiveElement as _, IntoElement,
|
||||
@@ -21,6 +21,7 @@ pub use panel::*;
|
||||
pub use stack_panel::*;
|
||||
pub use state::*;
|
||||
pub use tab_panel::*;
|
||||
pub use tiles::*;
|
||||
|
||||
use crate::theme::ActiveTheme;
|
||||
|
||||
@@ -74,7 +75,7 @@ pub struct DockArea {
|
||||
pub enum DockItem {
|
||||
/// Split layout
|
||||
Split {
|
||||
axis: gpui::Axis,
|
||||
axis: Axis,
|
||||
items: Vec<DockItem>,
|
||||
sizes: Vec<Option<Pixels>>,
|
||||
view: View<StackPanel>,
|
||||
@@ -87,6 +88,11 @@ pub enum DockItem {
|
||||
},
|
||||
/// Panel layout
|
||||
Panel { view: Arc<dyn PanelView> },
|
||||
/// Tiles layout
|
||||
Tiles {
|
||||
items: Vec<TileItem>,
|
||||
view: View<Tiles>,
|
||||
},
|
||||
}
|
||||
|
||||
impl DockItem {
|
||||
@@ -153,6 +159,51 @@ impl DockItem {
|
||||
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.
|
||||
///
|
||||
/// 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.
|
||||
fn view(&self) -> Arc<dyn PanelView> {
|
||||
pub fn view(&self) -> Arc<dyn PanelView> {
|
||||
match self {
|
||||
Self::Split { view, .. } => Arc::new(view.clone()),
|
||||
Self::Tabs { view, .. } => Arc::new(view.clone()),
|
||||
Self::Tiles { view, .. } => Arc::new(view.clone()),
|
||||
Self::Panel { view, .. } => view.clone(),
|
||||
}
|
||||
}
|
||||
@@ -217,6 +269,14 @@ impl DockItem {
|
||||
}
|
||||
Self::Tabs { items, .. } => items.iter().find(|item| *item == &panel).cloned(),
|
||||
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);
|
||||
});
|
||||
}
|
||||
Self::Tiles { .. } => {}
|
||||
Self::Panel { .. } => {}
|
||||
}
|
||||
}
|
||||
@@ -269,6 +330,7 @@ impl DockItem {
|
||||
item.set_collapsed(collapsed, cx);
|
||||
}
|
||||
}
|
||||
DockItem::Tiles { .. } => {}
|
||||
DockItem::Panel { .. } => {}
|
||||
}
|
||||
}
|
||||
@@ -278,6 +340,7 @@ impl DockItem {
|
||||
match self {
|
||||
DockItem::Tabs { view, .. } => Some(view.clone()),
|
||||
DockItem::Split { view, .. } => view.read(cx).left_top_tab_panel(true, cx),
|
||||
DockItem::Tiles { .. } => None,
|
||||
DockItem::Panel { .. } => None,
|
||||
}
|
||||
}
|
||||
@@ -287,6 +350,7 @@ impl DockItem {
|
||||
match self {
|
||||
DockItem::Tabs { view, .. } => Some(view.clone()),
|
||||
DockItem::Split { view, .. } => view.read(cx).right_top_tab_panel(true, cx),
|
||||
DockItem::Tiles { .. } => None,
|
||||
DockItem::Panel { .. } => None,
|
||||
}
|
||||
}
|
||||
@@ -601,7 +665,7 @@ impl DockArea {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Dump the dock panels layout to DockItemState.
|
||||
/// Dump the dock panels layout to PanelState.
|
||||
///
|
||||
/// See also [DockArea::load].
|
||||
pub fn dump(&self, cx: &AppContext) -> DockAreaState {
|
||||
@@ -658,6 +722,9 @@ impl DockArea {
|
||||
DockItem::Tabs { .. } => {
|
||||
// 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 { .. } => {
|
||||
// Not supported
|
||||
}
|
||||
@@ -727,6 +794,7 @@ impl DockArea {
|
||||
match &self.items {
|
||||
DockItem::Split { 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(),
|
||||
}
|
||||
}
|
||||
@@ -774,41 +842,50 @@ impl Render for DockArea {
|
||||
if let Some(zoom_view) = self.zoom_view.clone() {
|
||||
this.child(zoom_view)
|
||||
} else {
|
||||
this.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
.h_full()
|
||||
// Left dock
|
||||
.when_some(self.left_dock.clone(), |this, dock| {
|
||||
this.child(div().flex().flex_none().child(dock))
|
||||
.bg(cx.theme().sidebar)
|
||||
.text_color(cx.theme().sidebar_foreground)
|
||||
})
|
||||
// Center
|
||||
.child(
|
||||
match &self.items {
|
||||
DockItem::Tiles { view, .. } => {
|
||||
// render tiles
|
||||
this.child(view.clone())
|
||||
}
|
||||
_ => {
|
||||
// render dock
|
||||
this.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.flex_col()
|
||||
.overflow_hidden()
|
||||
// Top center
|
||||
.flex_row()
|
||||
.h_full()
|
||||
// Left dock
|
||||
.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(
|
||||
div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.flex_col()
|
||||
.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
|
||||
.when_some(self.bottom_dock.clone(), |this, dock| {
|
||||
this.child(dock)
|
||||
// Right Dock
|
||||
.when_some(self.right_dock.clone(), |this, 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))
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use gpui::{
|
||||
};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use super::{DockArea, DockItemInfo, DockItemState};
|
||||
use super::{DockArea, PanelInfo, PanelState};
|
||||
use crate::{button::Button, popup_menu::PopupMenu};
|
||||
|
||||
pub enum PanelEvent {
|
||||
@@ -36,7 +36,7 @@ pub trait Panel: EventEmitter<PanelEvent> + FocusableView {
|
||||
|
||||
/// The title of the panel
|
||||
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`.
|
||||
@@ -65,13 +65,13 @@ pub trait Panel: EventEmitter<PanelEvent> + FocusableView {
|
||||
}
|
||||
|
||||
/// Dump the panel, used to serialize the panel.
|
||||
fn dump(&self, _cx: &AppContext) -> DockItemState {
|
||||
DockItemState::new(self)
|
||||
fn dump(&self, _cx: &AppContext) -> PanelState {
|
||||
PanelState::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
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_style(&self, _cx: &WindowContext) -> Option<TitleStyle>;
|
||||
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 view(&self) -> AnyView;
|
||||
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> {
|
||||
fn panel_name(&self, cx: &WindowContext) -> &'static str {
|
||||
fn panel_name(&self, cx: &AppContext) -> &'static str {
|
||||
self.read(cx).panel_name()
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ impl<T: Panel> PanelView for View<T> {
|
||||
self.read(cx).focus_handle(cx)
|
||||
}
|
||||
|
||||
fn dump(&self, cx: &AppContext) -> DockItemState {
|
||||
fn dump(&self, cx: &AppContext) -> PanelState {
|
||||
self.read(cx).dump(cx)
|
||||
}
|
||||
}
|
||||
@@ -143,20 +143,20 @@ impl PartialEq for dyn PanelView {
|
||||
}
|
||||
}
|
||||
|
||||
type PanelRegistryItem = HashMap<
|
||||
type Items = HashMap<
|
||||
String,
|
||||
Arc<
|
||||
dyn Fn(
|
||||
WeakView<DockArea>,
|
||||
&DockItemState,
|
||||
&DockItemInfo,
|
||||
&PanelState,
|
||||
&PanelInfo,
|
||||
&mut WindowContext,
|
||||
) -> Box<dyn PanelView>,
|
||||
>,
|
||||
>;
|
||||
|
||||
pub struct PanelRegistry {
|
||||
pub(super) items: PanelRegistryItem,
|
||||
pub(super) items: Items,
|
||||
}
|
||||
|
||||
impl PanelRegistry {
|
||||
@@ -178,12 +178,7 @@ impl Global for PanelRegistry {}
|
||||
/// Register the Panel init by panel_name to global registry.
|
||||
pub fn register_panel<F>(cx: &mut AppContext, panel_name: &str, deserialize: F)
|
||||
where
|
||||
F: Fn(
|
||||
WeakView<DockArea>,
|
||||
&DockItemState,
|
||||
&DockItemInfo,
|
||||
&mut WindowContext,
|
||||
) -> Box<dyn PanelView>
|
||||
F: Fn(WeakView<DockArea>, &PanelState, &PanelInfo, &mut WindowContext) -> Box<dyn PanelView>
|
||||
+ 'static,
|
||||
{
|
||||
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 super::{DockArea, Panel, PanelEvent, PanelState, PanelView, TabPanel};
|
||||
use crate::{
|
||||
dock::DockItemInfo,
|
||||
dock::PanelInfo,
|
||||
h_flex,
|
||||
resizable::{
|
||||
h_resizable, resizable_panel, v_resizable, ResizablePanel, ResizablePanelEvent,
|
||||
@@ -11,14 +18,6 @@ use crate::{
|
||||
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(super) parent: Option<WeakView<StackPanel>>,
|
||||
pub(super) axis: Axis,
|
||||
@@ -37,12 +36,12 @@ impl Panel for StackPanel {
|
||||
"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 mut state = DockItemState::new(self);
|
||||
let mut state = PanelState::new(self);
|
||||
for panel in &self.panels {
|
||||
state.add_child(panel.dump(cx));
|
||||
state.info = DockItemInfo::stack(sizes.clone(), self.axis);
|
||||
state.info = PanelInfo::stack(sizes.clone(), self.axis);
|
||||
}
|
||||
|
||||
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 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.
|
||||
#[serde(default)]
|
||||
pub version: Option<usize>,
|
||||
pub center: DockItemState,
|
||||
pub center: PanelState,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub left_dock: Option<DockState>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub right_dock: Option<DockState>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub bottom_dock: Option<DockState>,
|
||||
}
|
||||
|
||||
/// Used to serialize and deserialize the Dock
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct DockState {
|
||||
panel: DockItemState,
|
||||
panel: PanelState,
|
||||
placement: DockPlacement,
|
||||
size: Pixels,
|
||||
open: bool,
|
||||
@@ -59,27 +65,52 @@ impl DockState {
|
||||
|
||||
/// Used to serialize and deserialize the DockerItem
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct DockItemState {
|
||||
pub struct PanelState {
|
||||
pub panel_name: String,
|
||||
pub children: Vec<DockItemState>,
|
||||
pub info: DockItemInfo,
|
||||
pub children: Vec<PanelState>,
|
||||
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)]
|
||||
pub enum DockItemInfo {
|
||||
pub enum PanelInfo {
|
||||
#[serde(rename = "stack")]
|
||||
Stack {
|
||||
sizes: Vec<Pixels>,
|
||||
/// The axis of the stack, 0 is horizontal, 1 is vertical
|
||||
axis: usize,
|
||||
axis: usize, // 0 for horizontal, 1 for vertical
|
||||
},
|
||||
#[serde(rename = "tabs")]
|
||||
Tabs { active_index: usize },
|
||||
#[serde(rename = "panel")]
|
||||
Panel(serde_json::Value),
|
||||
#[serde(rename = "tiles")]
|
||||
Tiles { metas: Vec<TileMeta> },
|
||||
}
|
||||
|
||||
impl DockItemInfo {
|
||||
impl PanelInfo {
|
||||
pub fn stack(sizes: Vec<Pixels>, axis: Axis) -> Self {
|
||||
Self::Stack {
|
||||
sizes,
|
||||
@@ -91,8 +122,12 @@ impl DockItemInfo {
|
||||
Self::Tabs { active_index }
|
||||
}
|
||||
|
||||
pub fn panel(value: serde_json::Value) -> Self {
|
||||
Self::Panel(value)
|
||||
pub fn panel(info: serde_json::Value) -> Self {
|
||||
Self::Panel(info)
|
||||
}
|
||||
|
||||
pub fn tiles(metas: Vec<TileMeta>) -> Self {
|
||||
Self::Tiles { metas }
|
||||
}
|
||||
|
||||
pub fn axis(&self) -> Option<Axis> {
|
||||
@@ -121,17 +156,17 @@ impl DockItemInfo {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DockItemState {
|
||||
impl Default for PanelState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
panel_name: "".to_string(),
|
||||
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 {
|
||||
Self {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -153,7 +188,7 @@ impl DockItemState {
|
||||
.collect();
|
||||
|
||||
match info {
|
||||
DockItemInfo::Stack { sizes, axis } => {
|
||||
PanelInfo::Stack { sizes, axis } => {
|
||||
let axis = if axis == 0 {
|
||||
Axis::Horizontal
|
||||
} else {
|
||||
@@ -162,7 +197,7 @@ impl DockItemState {
|
||||
let sizes = sizes.iter().map(|s| Some(*s)).collect_vec();
|
||||
DockItem::split_with_sizes(axis, items, sizes, &dock_area, cx)
|
||||
}
|
||||
DockItemInfo::Tabs { active_index } => {
|
||||
PanelInfo::Tabs { active_index } => {
|
||||
if items.len() == 1 {
|
||||
return items[0].clone();
|
||||
}
|
||||
@@ -172,14 +207,15 @@ impl DockItemState {
|
||||
.flat_map(|item| match item {
|
||||
DockItem::Tabs { items, .. } => items.clone(),
|
||||
_ => {
|
||||
unreachable!("Invalid DockItem type in DockItemInfo::Tabs")
|
||||
// ignore invalid panels in tabs
|
||||
vec![]
|
||||
}
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
DockItem::tabs(items, Some(active_index), &dock_area, cx)
|
||||
}
|
||||
DockItemInfo::Panel(_) => {
|
||||
PanelInfo::Panel(_) => {
|
||||
let view = if let Some(f) = cx
|
||||
.global::<PanelRegistry>()
|
||||
.items
|
||||
@@ -196,6 +232,7 @@ impl DockItemState {
|
||||
|
||||
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 super::{
|
||||
ClosePanel, DockArea, DockItemState, DockPlacement, Panel, PanelEvent, PanelStyle, PanelView,
|
||||
ClosePanel, DockArea, DockPlacement, Panel, PanelEvent, PanelState, PanelStyle, PanelView,
|
||||
StackPanel, ToggleZoom,
|
||||
};
|
||||
use crate::{
|
||||
button::{Button, ButtonVariants as _},
|
||||
dock::DockItemInfo,
|
||||
dock::PanelInfo,
|
||||
h_flex,
|
||||
popup_menu::{PopupMenu, PopupMenuExt},
|
||||
tab::{Tab, TabBar},
|
||||
@@ -122,11 +122,11 @@ impl Panel for TabPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn dump(&self, cx: &AppContext) -> DockItemState {
|
||||
let mut state = DockItemState::new(self);
|
||||
fn dump(&self, cx: &AppContext) -> PanelState {
|
||||
let mut state = PanelState::new(self);
|
||||
for panel in self.panels.iter() {
|
||||
state.add_child(panel.dump(cx));
|
||||
state.info = DockItemInfo::tabs(self.active_ix);
|
||||
state.info = PanelInfo::tabs(self.active_ix);
|
||||
}
|
||||
state
|
||||
}
|
||||
@@ -720,13 +720,11 @@ impl TabPanel {
|
||||
|
||||
// If target is same tab, and it is only one panel, do nothing.
|
||||
if is_same_tab && ix.is_none() {
|
||||
#[allow(clippy::if_same_then_else)]
|
||||
if self.will_split_placement.is_none() {
|
||||
return;
|
||||
} else {
|
||||
#[allow(clippy::collapsible_else_if)]
|
||||
if self.panels.len() == 1 {
|
||||
return;
|
||||
}
|
||||
} else if self.panels.len() == 1 {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -888,8 +886,11 @@ impl FocusableView for TabPanel {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for TabPanel {}
|
||||
|
||||
impl EventEmitter<PanelEvent> for TabPanel {}
|
||||
|
||||
impl Render for TabPanel {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl gpui::IntoElement {
|
||||
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,
|
||||
TriangleAlert,
|
||||
Upload,
|
||||
ResizeCorner,
|
||||
WindowClose,
|
||||
WindowMaximize,
|
||||
WindowMinimize,
|
||||
@@ -84,79 +85,80 @@ pub enum IconName {
|
||||
impl IconName {
|
||||
pub fn path(self) -> SharedString {
|
||||
match self {
|
||||
IconName::ALargeSmall => "icons/a-large-small.svg",
|
||||
IconName::ArrowDown => "icons/arrow-down.svg",
|
||||
IconName::ArrowLeft => "icons/arrow-left.svg",
|
||||
IconName::ArrowRight => "icons/arrow-right.svg",
|
||||
IconName::ArrowUp => "icons/arrow-up.svg",
|
||||
IconName::ArrowUpCircle => "icons/arrow-up-circle.svg",
|
||||
IconName::Asterisk => "icons/asterisk.svg",
|
||||
IconName::Bell => "icons/bell.svg",
|
||||
IconName::BookOpen => "icons/book-open.svg",
|
||||
IconName::Bot => "icons/bot.svg",
|
||||
IconName::Calendar => "icons/calendar.svg",
|
||||
IconName::ChartPie => "icons/chart-pie.svg",
|
||||
IconName::Check => "icons/check.svg",
|
||||
IconName::ChevronDown => "icons/chevron-down.svg",
|
||||
IconName::ChevronLeft => "icons/chevron-left.svg",
|
||||
IconName::ChevronRight => "icons/chevron-right.svg",
|
||||
IconName::ChevronUp => "icons/chevron-up.svg",
|
||||
IconName::ChevronsUpDown => "icons/chevrons-up-down.svg",
|
||||
IconName::CircleCheck => "icons/circle-check.svg",
|
||||
IconName::CircleUser => "icons/circle-user.svg",
|
||||
IconName::CircleX => "icons/circle-x.svg",
|
||||
IconName::Close => "icons/close.svg",
|
||||
IconName::Copy => "icons/copy.svg",
|
||||
IconName::Dash => "icons/dash.svg",
|
||||
IconName::Delete => "icons/delete.svg",
|
||||
IconName::Ellipsis => "icons/ellipsis.svg",
|
||||
IconName::EllipsisVertical => "icons/ellipsis-vertical.svg",
|
||||
IconName::Eye => "icons/eye.svg",
|
||||
IconName::EyeOff => "icons/eye-off.svg",
|
||||
IconName::Frame => "icons/frame.svg",
|
||||
IconName::GalleryVerticalEnd => "icons/gallery-vertical-end.svg",
|
||||
IconName::GitHub => "icons/github.svg",
|
||||
IconName::Globe => "icons/globe.svg",
|
||||
IconName::Heart => "icons/heart.svg",
|
||||
IconName::HeartOff => "icons/heart-off.svg",
|
||||
IconName::Inbox => "icons/inbox.svg",
|
||||
IconName::Info => "icons/info.svg",
|
||||
IconName::LayoutDashboard => "icons/layout-dashboard.svg",
|
||||
IconName::Loader => "icons/loader.svg",
|
||||
IconName::LoaderCircle => "icons/loader-circle.svg",
|
||||
IconName::Map => "icons/map.svg",
|
||||
IconName::Maximize => "icons/maximize.svg",
|
||||
IconName::Menu => "icons/menu.svg",
|
||||
IconName::Minimize => "icons/minimize.svg",
|
||||
IconName::Minus => "icons/minus.svg",
|
||||
IconName::Moon => "icons/moon.svg",
|
||||
IconName::Palette => "icons/palette.svg",
|
||||
IconName::PanelBottom => "icons/panel-bottom.svg",
|
||||
IconName::PanelBottomOpen => "icons/panel-bottom-open.svg",
|
||||
IconName::PanelLeft => "icons/panel-left.svg",
|
||||
IconName::PanelLeftClose => "icons/panel-left-close.svg",
|
||||
IconName::PanelLeftOpen => "icons/panel-left-open.svg",
|
||||
IconName::PanelRight => "icons/panel-right.svg",
|
||||
IconName::PanelRightClose => "icons/panel-right-close.svg",
|
||||
IconName::PanelRightOpen => "icons/panel-right-open.svg",
|
||||
IconName::Plus => "icons/plus.svg",
|
||||
IconName::Search => "icons/search.svg",
|
||||
IconName::Settings => "icons/settings.svg",
|
||||
IconName::Settings2 => "icons/settings-2.svg",
|
||||
IconName::SortAscending => "icons/sort-ascending.svg",
|
||||
IconName::SortDescending => "icons/sort-descending.svg",
|
||||
IconName::SquareTerminal => "icons/square-terminal.svg",
|
||||
IconName::Star => "icons/star.svg",
|
||||
IconName::StarOff => "icons/star-off.svg",
|
||||
IconName::Sun => "icons/sun.svg",
|
||||
IconName::ThumbsDown => "icons/thumbs-down.svg",
|
||||
IconName::ThumbsUp => "icons/thumbs-up.svg",
|
||||
IconName::TriangleAlert => "icons/triangle-alert.svg",
|
||||
IconName::Upload => "icons/upload.svg",
|
||||
IconName::WindowClose => "icons/window-close.svg",
|
||||
IconName::WindowMaximize => "icons/window-maximize.svg",
|
||||
IconName::WindowMinimize => "icons/window-minimize.svg",
|
||||
IconName::WindowRestore => "icons/window-restore.svg",
|
||||
Self::ALargeSmall => "icons/a-large-small.svg",
|
||||
Self::ArrowDown => "icons/arrow-down.svg",
|
||||
Self::ArrowLeft => "icons/arrow-left.svg",
|
||||
Self::ArrowRight => "icons/arrow-right.svg",
|
||||
Self::ArrowUp => "icons/arrow-up.svg",
|
||||
Self::ArrowUpCircle => "icons/arrow-up-circle.svg",
|
||||
Self::Asterisk => "icons/asterisk.svg",
|
||||
Self::Bell => "icons/bell.svg",
|
||||
Self::BookOpen => "icons/book-open.svg",
|
||||
Self::Bot => "icons/bot.svg",
|
||||
Self::Calendar => "icons/calendar.svg",
|
||||
Self::ChartPie => "icons/chart-pie.svg",
|
||||
Self::Check => "icons/check.svg",
|
||||
Self::ChevronDown => "icons/chevron-down.svg",
|
||||
Self::ChevronLeft => "icons/chevron-left.svg",
|
||||
Self::ChevronRight => "icons/chevron-right.svg",
|
||||
Self::ChevronUp => "icons/chevron-up.svg",
|
||||
Self::ChevronsUpDown => "icons/chevrons-up-down.svg",
|
||||
Self::CircleCheck => "icons/circle-check.svg",
|
||||
Self::CircleUser => "icons/circle-user.svg",
|
||||
Self::CircleX => "icons/circle-x.svg",
|
||||
Self::Close => "icons/close.svg",
|
||||
Self::Copy => "icons/copy.svg",
|
||||
Self::Dash => "icons/dash.svg",
|
||||
Self::Delete => "icons/delete.svg",
|
||||
Self::Ellipsis => "icons/ellipsis.svg",
|
||||
Self::EllipsisVertical => "icons/ellipsis-vertical.svg",
|
||||
Self::Eye => "icons/eye.svg",
|
||||
Self::EyeOff => "icons/eye-off.svg",
|
||||
Self::Frame => "icons/frame.svg",
|
||||
Self::GalleryVerticalEnd => "icons/gallery-vertical-end.svg",
|
||||
Self::GitHub => "icons/github.svg",
|
||||
Self::Globe => "icons/globe.svg",
|
||||
Self::Heart => "icons/heart.svg",
|
||||
Self::HeartOff => "icons/heart-off.svg",
|
||||
Self::Inbox => "icons/inbox.svg",
|
||||
Self::Info => "icons/info.svg",
|
||||
Self::LayoutDashboard => "icons/layout-dashboard.svg",
|
||||
Self::Loader => "icons/loader.svg",
|
||||
Self::LoaderCircle => "icons/loader-circle.svg",
|
||||
Self::Map => "icons/map.svg",
|
||||
Self::Maximize => "icons/maximize.svg",
|
||||
Self::Menu => "icons/menu.svg",
|
||||
Self::Minimize => "icons/minimize.svg",
|
||||
Self::Minus => "icons/minus.svg",
|
||||
Self::Moon => "icons/moon.svg",
|
||||
Self::Palette => "icons/palette.svg",
|
||||
Self::PanelBottom => "icons/panel-bottom.svg",
|
||||
Self::PanelBottomOpen => "icons/panel-bottom-open.svg",
|
||||
Self::PanelLeft => "icons/panel-left.svg",
|
||||
Self::PanelLeftClose => "icons/panel-left-close.svg",
|
||||
Self::PanelLeftOpen => "icons/panel-left-open.svg",
|
||||
Self::PanelRight => "icons/panel-right.svg",
|
||||
Self::PanelRightClose => "icons/panel-right-close.svg",
|
||||
Self::PanelRightOpen => "icons/panel-right-open.svg",
|
||||
Self::Plus => "icons/plus.svg",
|
||||
Self::Search => "icons/search.svg",
|
||||
Self::Settings => "icons/settings.svg",
|
||||
Self::Settings2 => "icons/settings-2.svg",
|
||||
Self::SortAscending => "icons/sort-ascending.svg",
|
||||
Self::SortDescending => "icons/sort-descending.svg",
|
||||
Self::SquareTerminal => "icons/square-terminal.svg",
|
||||
Self::Star => "icons/star.svg",
|
||||
Self::StarOff => "icons/star-off.svg",
|
||||
Self::Sun => "icons/sun.svg",
|
||||
Self::ThumbsDown => "icons/thumbs-down.svg",
|
||||
Self::ThumbsUp => "icons/thumbs-up.svg",
|
||||
Self::TriangleAlert => "icons/triangle-alert.svg",
|
||||
Self::Upload => "icons/upload.svg",
|
||||
Self::ResizeCorner => "icons/resize-corner.svg",
|
||||
Self::WindowClose => "icons/window-close.svg",
|
||||
Self::WindowMaximize => "icons/window-maximize.svg",
|
||||
Self::WindowMinimize => "icons/window-minimize.svg",
|
||||
Self::WindowRestore => "icons/window-restore.svg",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user