Skip to content

Commit 39656d4

Browse files
authored
Refactor panel layouts to generalize recursive panel group subdivision splits (#4014)
* Generalize recursive panel group splits * Code review
1 parent 0eb440d commit 39656d4

File tree

10 files changed

+660
-445
lines changed

10 files changed

+660
-445
lines changed

desktop/src/cef/consts.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use graphite_desktop_wrapper::DOUBLE_CLICK_MILLISECONDS;
12
use std::time::Duration;
23

34
pub(crate) const RESOURCE_SCHEME: &str = "resources";
@@ -18,5 +19,5 @@ pub(crate) const SCROLL_SPEED_Y: f32 = 1.0;
1819

1920
pub(crate) const PINCH_ZOOM_SPEED: f64 = 300.0;
2021

21-
pub(crate) const MULTICLICK_TIMEOUT: Duration = Duration::from_millis(500);
22+
pub(crate) const MULTICLICK_TIMEOUT: Duration = Duration::from_millis(DOUBLE_CLICK_MILLISECONDS);
2223
pub(crate) const MULTICLICK_ALLOWED_TRAVEL: usize = 4;

desktop/wrapper/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use graphite_editor::messages::prelude::{FrontendMessage, Message};
44
use message_dispatcher::DesktopWrapperMessageDispatcher;
55
use messages::{DesktopFrontendMessage, DesktopWrapperMessage};
66

7-
pub use graphite_editor::consts::FILE_EXTENSION;
7+
pub use graphite_editor::consts::{DOUBLE_CLICK_MILLISECONDS, FILE_EXTENSION};
88
pub use wgpu_executor::TargetTexture;
99
pub use wgpu_executor::WgpuContext;
1010
pub use wgpu_executor::WgpuContextBuilder;

editor/src/messages/portfolio/portfolio_message_handler.rs

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use super::document::utility_types::document_metadata::LayerNodeIdentifier;
22
use super::document::utility_types::network_interface;
3-
use super::utility_types::{PanelGroupId, PanelType, PersistentData, WorkspacePanelLayout};
3+
use super::utility_types::{PanelType, PersistentData, WorkspacePanelLayout};
44
use crate::application::{Editor, generate_uuid};
55
use crate::consts::{DEFAULT_DOCUMENT_NAME, DEFAULT_STROKE_WIDTH, FILE_EXTENSION};
66
use crate::messages::animation::TimingInformation;
@@ -469,26 +469,37 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
469469
return;
470470
}
471471

472-
let source_state = self.workspace_panel_layout.panel_group(source_group);
472+
let Some(source_state) = self.workspace_panel_layout.panel_group(source_group) else { return };
473473
let Some(panel_type) = source_state.active_panel_type() else { return };
474474

475+
// Validate that the target group exists before modifying the source
476+
if self.workspace_panel_layout.panel_group(target_group).is_none() {
477+
log::error!("Target panel group {target_group:?} not found");
478+
return;
479+
}
480+
475481
// Destroy layouts for the moved panel (so backend and frontend start in sync when it remounts)
476482
// and for the panel that was previously active in the target panel group (it will be displaced by the incoming tab)
477483
Self::destroy_panel_layouts(panel_type, responses);
478-
if let Some(old_target_panel) = self.workspace_panel_layout.panel_group(target_group).active_panel_type() {
484+
if let Some(old_target_panel) = self.workspace_panel_layout.panel_group(target_group).and_then(|g| g.active_panel_type()) {
479485
Self::destroy_panel_layouts(old_target_panel, responses);
480486
}
481487

482488
// Remove from source panel group
483-
let source = self.workspace_panel_layout.panel_group_mut(source_group);
484-
source.tabs.retain(|&t| t != panel_type);
485-
source.active_tab_index = source.active_tab_index.min(source.tabs.len().saturating_sub(1));
489+
if let Some(source) = self.workspace_panel_layout.panel_group_mut(source_group) {
490+
source.tabs.retain(|&t| t != panel_type);
491+
source.active_tab_index = source.active_tab_index.min(source.tabs.len().saturating_sub(1));
492+
}
486493

487494
// Insert into target panel group
488-
let target = self.workspace_panel_layout.panel_group_mut(target_group);
489-
let index = insert_index.min(target.tabs.len());
490-
target.tabs.insert(index, panel_type);
491-
target.active_tab_index = index;
495+
if let Some(target) = self.workspace_panel_layout.panel_group_mut(target_group) {
496+
let index = insert_index.min(target.tabs.len());
497+
target.tabs.insert(index, panel_type);
498+
target.active_tab_index = index;
499+
}
500+
501+
// Remove empty panel groups from the tree
502+
self.workspace_panel_layout.prune();
492503

493504
responses.add(MenuBarMessage::SendLayout);
494505
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
@@ -497,7 +508,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
497508
self.refresh_panel_content(panel_type, responses);
498509

499510
// Refresh the source panel group's newly active tab (if any remain) so it's not left stale
500-
if let Some(new_source_active) = self.workspace_panel_layout.panel_group(source_group).active_panel_type() {
511+
if let Some(new_source_active) = self.workspace_panel_layout.panel_group(source_group).and_then(|g| g.active_panel_type()) {
501512
Self::destroy_panel_layouts(new_source_active, responses);
502513
self.refresh_panel_content(new_source_active, responses);
503514
}
@@ -1110,7 +1121,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
11101121
}
11111122
}
11121123
PortfolioMessage::ReorderPanelGroupTab { group, old_index, new_index } => {
1113-
let group_state = self.workspace_panel_layout.panel_group_mut(group);
1124+
let Some(group_state) = self.workspace_panel_layout.panel_group_mut(group) else { return };
11141125

11151126
if old_index < group_state.tabs.len() && new_index < group_state.tabs.len() && old_index != new_index {
11161127
let tab = group_state.tabs.remove(old_index);
@@ -1189,7 +1200,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
11891200
});
11901201
}
11911202
PortfolioMessage::SetPanelGroupActiveTab { group, tab_index } => {
1192-
let group_state = self.workspace_panel_layout.panel_group(group);
1203+
let Some(group_state) = self.workspace_panel_layout.panel_group(group) else { return };
11931204
if tab_index < group_state.tabs.len() && tab_index != group_state.active_tab_index {
11941205
// Destroy layouts for the old and new panels so the backend's diffing state is in sync with the frontend's fresh mount
11951206
if let Some(old_panel_type) = group_state.active_panel_type() {
@@ -1199,12 +1210,14 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
11991210
Self::destroy_panel_layouts(new_panel_type, responses);
12001211

12011212
// Update the active tab index for the panel
1202-
self.workspace_panel_layout.panel_group_mut(group).active_tab_index = tab_index;
1213+
if let Some(group_state) = self.workspace_panel_layout.panel_group_mut(group) {
1214+
group_state.active_tab_index = tab_index;
1215+
}
12031216

12041217
// Send the layout update first so the frontend mounts the new panel component before it receives content
12051218
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
12061219

1207-
if let Some(panel_type) = self.workspace_panel_layout.panel_group(group).active_panel_type() {
1220+
if let Some(panel_type) = self.workspace_panel_layout.panel_group(group).and_then(|g| g.active_panel_type()) {
12081221
self.refresh_panel_content(panel_type, responses);
12091222
}
12101223
}
@@ -1433,6 +1446,8 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
14331446
}
14341447
}
14351448
PortfolioMessage::UpdateWorkspacePanelLayout => {
1449+
self.workspace_panel_layout.recalculate_default_sizes();
1450+
14361451
responses.add(FrontendMessage::UpdateWorkspacePanelLayout {
14371452
panel_layout: self.workspace_panel_layout.clone(),
14381453
});
@@ -1652,50 +1667,44 @@ impl PortfolioMessageHandler {
16521667
selected_nodes.first().copied()
16531668
}
16541669

1655-
/// Remove a dockable panel type from whichever panel group currently contains it.
1670+
/// Remove a dockable panel type from whichever panel group currently contains it, then prune empty groups.
16561671
fn remove_panel_from_layout(&mut self, panel_type: PanelType) {
1657-
for group_id in [PanelGroupId::PropertiesGroup, PanelGroupId::LayersGroup, PanelGroupId::DataGroup] {
1658-
let group = self.workspace_panel_layout.panel_group_mut(group_id);
1659-
if let Some(index) = group.tabs.iter().position(|&t| t == panel_type) {
1660-
group.tabs.remove(index);
1661-
group.active_tab_index = group.active_tab_index.min(group.tabs.len().saturating_sub(1));
1662-
break;
1663-
}
1672+
// Save the panel's current position so it can be restored there later
1673+
self.workspace_panel_layout.save_panel_position(panel_type);
1674+
1675+
if let Some(group_id) = self.workspace_panel_layout.find_panel(panel_type)
1676+
&& let Some(group) = self.workspace_panel_layout.panel_group_mut(group_id)
1677+
{
1678+
group.tabs.retain(|&t| t != panel_type);
1679+
group.active_tab_index = group.active_tab_index.min(group.tabs.len().saturating_sub(1));
16641680
}
1681+
1682+
self.workspace_panel_layout.prune();
16651683
}
16661684

16671685
/// Toggle a dockable panel on or off. When toggling off, refresh the newly active tab in its panel group (if any).
16681686
fn toggle_dockable_panel(&mut self, panel_type: PanelType, responses: &mut VecDeque<Message>) {
16691687
if let Some(group_id) = self.workspace_panel_layout.find_panel(panel_type) {
16701688
// Panel is present, remove it
1671-
let was_visible = self.workspace_panel_layout.panel_group(group_id).is_visible(panel_type);
1689+
let was_visible = self.workspace_panel_layout.panel_group(group_id).is_some_and(|g| g.is_visible(panel_type));
16721690
Self::destroy_panel_layouts(panel_type, responses);
16731691
self.remove_panel_from_layout(panel_type);
16741692

16751693
// If the removed panel was the active tab, refresh whichever panel is now active in that panel group
1676-
if was_visible && let Some(new_active) = self.workspace_panel_layout.panel_group(group_id).active_panel_type() {
1694+
if was_visible && let Some(new_active) = self.workspace_panel_layout.panel_group(group_id).and_then(|g| g.active_panel_type()) {
16771695
Self::destroy_panel_layouts(new_active, responses);
16781696
self.refresh_panel_content(new_active, responses);
16791697
}
16801698
} else {
1681-
// Panel is not present, add it to its default panel group
1682-
self.add_panel_to_its_default_group(panel_type);
1699+
// Panel is not present, restore it to its default position in the layout tree
1700+
self.workspace_panel_layout.restore_panel(panel_type);
16831701
self.refresh_panel_content(panel_type, responses);
16841702
}
16851703

16861704
responses.add(MenuBarMessage::SendLayout);
16871705
responses.add(PortfolioMessage::UpdateWorkspacePanelLayout);
16881706
}
16891707

1690-
/// Add a dockable panel type to its default panel group.
1691-
fn add_panel_to_its_default_group(&mut self, panel_type: PanelType) {
1692-
let group = self.workspace_panel_layout.panel_group_mut(panel_type.default_panel_group());
1693-
if !group.tabs.contains(&panel_type) {
1694-
group.tabs.push(panel_type);
1695-
group.active_tab_index = group.tabs.len() - 1;
1696-
}
1697-
}
1698-
16991708
/// Destroy the stored layout for a panel that is no longer the active tab.
17001709
/// This resets the backend's diffing state so it won't try to send updates to a frontend component that has been unmounted.
17011710
fn destroy_panel_layouts(panel_type: PanelType, responses: &mut VecDeque<Message>) {

0 commit comments

Comments
 (0)