mirror of
https://github.com/storytold/craft-libs.git
synced 2026-10-31 07:39:23 +00:00
docking: Workspace with hidden locations, lock, reconcile and reset
Workspace<P> keeps the current layout, where hidden panels go back, the
user's saved arrangement and Lock Workspace. It reads the {layout,
hidden} files lightcraft, pdfcraft, cadcraft and soundcraft write today.
Close actions record a location per closed panel; set_visible follows
hidden anchors when a panel's own anchor is hidden too. reconcile moves
panels the app does not register now to hidden and places missing
registered panels at their default place; reset returns to the saved or
default arrangement.
This commit is contained in:
1 parent
0ae7cddbb5
commit
0ea5f8200c
2 files changed
+245
No files matched your search
@@ -9,11 +9,13 @@ mod dock_model;
|
||||
mod model;
|
||||
mod surface;
|
||||
mod targets;
|
||||
mod workspace;
|
||||
|
||||
pub use dock_model::{Column, ColumnLocation, Group, GroupLocation, PanelState, Side, Target};
|
||||
pub use model::{Action, DockError, Floating, Layout, Location, MAX_DEPTH, MAX_PANELS, Node, Permissions, Placement, StackEntry, Zone};
|
||||
pub use surface::{DockArea, DockContent, DockOutput, DockStyle, PanelLimits, begin_drag, cancel_drag};
|
||||
pub use targets::{DropProposal, DropTarget, drop_zone, resolve_drop, zone_rect};
|
||||
pub use workspace::Workspace;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,243 @@
|
||||
//! A saved arrangement: the current layout, where hidden panels go back, the user's saved
|
||||
//! arrangement for Reset, and Lock Workspace.
|
||||
//!
|
||||
//! Storage, file names and size caps stay with the application. A file that does not parse or
|
||||
//! validate, or comes from a newer version, should be kept on disk and not overwritten.
|
||||
|
||||
use std::{collections::HashSet, hash::Hash};
|
||||
|
||||
use super::model::{Action, DockError, Layout, Location, MAX_PANELS, Permissions};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
#[cfg_attr(feature = "serde", serde(default, bound(deserialize = "P: serde::Deserialize<'de>")))]
|
||||
pub struct Workspace<P> {
|
||||
/// Format version; files without one are v1 (`{layout, hidden}`). Always written as
|
||||
/// [`Workspace::VERSION`].
|
||||
#[cfg_attr(feature = "serde", serde(default = "first_version", serialize_with = "write_version"))]
|
||||
pub version: u32,
|
||||
/// The current arrangement, changes included.
|
||||
pub layout: Layout<P>,
|
||||
/// Closed panels, and panels the application does not register now, with where each one
|
||||
/// goes back.
|
||||
pub hidden: Vec<(P, Location<P>)>,
|
||||
/// A user workspace's saved arrangement; Reset returns to it. Built-in workspaces use the
|
||||
/// application's default instead.
|
||||
pub saved: Option<Layout<P>>,
|
||||
/// Lock Workspace, saved with this workspace: [`Self::apply`] uses
|
||||
/// [`Permissions::locked`], so moving, floating and closing are rejected while activating,
|
||||
/// opening, minimizing and resizing still work. An application with a global lock
|
||||
/// preference instead passes locked permissions itself.
|
||||
pub locked: bool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
fn first_version() -> u32 {
|
||||
1
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
fn write_version<S: serde::Serializer>(_: &u32, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
serializer.serialize_u32(Workspace::<()>::VERSION)
|
||||
}
|
||||
|
||||
impl<P> Default for Workspace<P> {
|
||||
fn default() -> Self {
|
||||
Self::new(Layout::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> Workspace<P> {
|
||||
pub const VERSION: u32 = 2;
|
||||
|
||||
pub fn new(layout: Layout<P>) -> Self {
|
||||
Self { version: Self::VERSION, layout, hidden: Vec::new(), saved: None, locked: false }
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Clone + Eq + Hash> Workspace<P> {
|
||||
/// Check the version, both layouts and that every hidden panel is hidden once and absent
|
||||
/// from the current layout.
|
||||
pub fn validate(&self) -> Result<(), DockError> {
|
||||
if self.version > Self::VERSION {
|
||||
return Err(DockError::UnsupportedVersion);
|
||||
}
|
||||
self.layout.validate()?;
|
||||
if let Some(saved) = &self.saved {
|
||||
saved.validate()?;
|
||||
}
|
||||
if self.hidden.len() > MAX_PANELS {
|
||||
return Err(DockError::LimitExceeded);
|
||||
}
|
||||
let visible: HashSet<&P> = self.layout.panels().into_iter().collect();
|
||||
let mut seen = HashSet::new();
|
||||
for (panel, _) in &self.hidden {
|
||||
if visible.contains(panel) || !seen.insert(panel) {
|
||||
return Err(DockError::DuplicatePanel);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply one action. Closed panels record where they go back, so each one can be reopened
|
||||
/// with [`Self::set_visible`]. Honours [`Self::locked`]. [`Layout::closed_by`], called
|
||||
/// before applying, names the panels a close action removes.
|
||||
pub fn apply(&mut self, action: Action<P>, permissions: impl Fn(&P) -> Permissions) -> Result<(), DockError> {
|
||||
self.apply_all([action], permissions)
|
||||
}
|
||||
|
||||
/// Apply every action in order, or none.
|
||||
pub fn apply_all(&mut self, actions: impl IntoIterator<Item = Action<P>>, permissions: impl Fn(&P) -> Permissions) -> Result<(), DockError> {
|
||||
self.validate()?;
|
||||
let locked = self.locked;
|
||||
let policy = |panel: &P| if locked { permissions(panel).locked() } else { permissions(panel) };
|
||||
let mut proposed = self.clone();
|
||||
for action in actions {
|
||||
let closed = proposed.layout.closed_by(&action)?;
|
||||
let locations = close_locations(&proposed.layout, &closed);
|
||||
proposed.layout.apply_with_permissions(action, policy)?;
|
||||
for (panel, location) in locations {
|
||||
proposed.hidden.retain(|(p, _)| p != &panel);
|
||||
proposed.hidden.push((panel, location));
|
||||
}
|
||||
}
|
||||
let visible: HashSet<P> = proposed.layout.panels().into_iter().cloned().collect();
|
||||
proposed.hidden.retain(|(p, _)| !visible.contains(p));
|
||||
proposed.validate()?;
|
||||
*self = proposed;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Show or hide one panel. Showing restores its recorded location, following the chain of
|
||||
/// hidden anchors when its own anchor is hidden too; otherwise it opens beside `fallback`.
|
||||
/// A shown panel becomes the active tab of its group.
|
||||
pub fn set_visible(&mut self, panel: P, visible: bool, fallback: Option<P>) -> Result<(), DockError> {
|
||||
self.validate()?;
|
||||
if !visible {
|
||||
if !self.layout.contains(&panel) {
|
||||
return Ok(());
|
||||
}
|
||||
return self.apply(Action::Close { panel }, |_| Permissions::default());
|
||||
}
|
||||
if self.layout.contains(&panel) {
|
||||
return self.apply(Action::Activate { panel }, |_| Permissions::default());
|
||||
}
|
||||
let mut layout = self.layout.clone();
|
||||
if self.restore_into(&mut layout, &panel).is_err() {
|
||||
layout = self.layout.clone();
|
||||
layout.apply(Action::Open { panel: panel.clone(), anchor: fallback })?;
|
||||
}
|
||||
layout.apply(Action::Activate { panel: panel.clone() })?;
|
||||
self.layout = layout;
|
||||
self.hidden.retain(|(p, _)| p != &panel);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restore_into(&self, layout: &mut Layout<P>, panel: &P) -> Result<(), DockError> {
|
||||
let find = |p: &P| self.hidden.iter().find(|(q, _)| q == p).map(|(_, location)| location);
|
||||
let mut location = find(panel).ok_or(DockError::MissingTarget)?;
|
||||
for _ in 0..=self.hidden.len() {
|
||||
match layout.restore(panel.clone(), location) {
|
||||
Err(DockError::MissingTarget) => {
|
||||
let next = location.anchor.as_ref().filter(|anchor| *anchor != panel).and_then(find);
|
||||
location = next.ok_or(DockError::MissingTarget)?;
|
||||
}
|
||||
result => return result,
|
||||
}
|
||||
}
|
||||
Err(DockError::MissingTarget)
|
||||
}
|
||||
|
||||
/// Fit a loaded workspace to the panels the application registers now. An invalid
|
||||
/// workspace is replaced by `default`. Panels not registered now (plug-ins, on-demand or
|
||||
/// numbered panels) move to `hidden` with their location, so they come back once registered;
|
||||
/// the saved arrangement drops them. Registered panels in `default` that are neither shown
|
||||
/// nor hidden go to their default place, or to `hidden` when that place cannot be rebuilt.
|
||||
/// `hidden` keeps at most [`MAX_PANELS`] entries, dropping the oldest unregistered ones first.
|
||||
pub fn reconcile(&mut self, registered: impl Fn(&P) -> bool, default: &Workspace<P>) {
|
||||
if self.validate().is_err() {
|
||||
*self = Workspace { locked: self.locked, ..default.clone() };
|
||||
}
|
||||
let unknown: Vec<P> = self.layout.panels().into_iter().filter(|p| !registered(p)).cloned().collect();
|
||||
let locations = close_locations(&self.layout, &unknown);
|
||||
for panel in &unknown {
|
||||
self.layout.take(panel);
|
||||
}
|
||||
self.hidden.extend(locations);
|
||||
if let Some(saved) = self.saved.as_mut() {
|
||||
let unknown: Vec<P> = saved.panels().into_iter().filter(|p| !registered(p)).cloned().collect();
|
||||
for panel in unknown {
|
||||
saved.take(&panel);
|
||||
}
|
||||
if saved.validate().is_err() {
|
||||
self.saved = None;
|
||||
}
|
||||
}
|
||||
let known: HashSet<P> = self.layout.panels().into_iter().chain(self.hidden.iter().map(|(p, _)| p)).cloned().collect();
|
||||
let mut missing: Vec<(P, Location<P>)> = Vec::new();
|
||||
for panel in default.layout.panels() {
|
||||
if registered(panel)
|
||||
&& !known.contains(panel)
|
||||
&& let Ok(location) = default.layout.location(panel)
|
||||
{
|
||||
missing.push((panel.clone(), location));
|
||||
}
|
||||
}
|
||||
// Anchors may themselves be missing: place what fits, then retry the rest.
|
||||
while !missing.is_empty() {
|
||||
let before = missing.len();
|
||||
missing.retain(|(panel, location)| self.layout.restore(panel.clone(), location).is_err());
|
||||
if missing.len() == before {
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.hidden.extend(missing);
|
||||
for (panel, location) in &default.hidden {
|
||||
if registered(panel) && !known.contains(panel) && !self.layout.contains(panel) && !self.hidden.iter().any(|(p, _)| p == panel) {
|
||||
self.hidden.push((panel.clone(), location.clone()));
|
||||
}
|
||||
}
|
||||
let mut excess = self.hidden.len().saturating_sub(MAX_PANELS);
|
||||
self.hidden.retain(|(p, _)| {
|
||||
let drop = excess > 0 && !registered(p);
|
||||
excess -= usize::from(drop);
|
||||
!drop
|
||||
});
|
||||
self.hidden.truncate(MAX_PANELS);
|
||||
}
|
||||
|
||||
/// Reset: return to the saved arrangement, or `default` when there is none. Panels the
|
||||
/// reset layout does not show stay hidden with their last location.
|
||||
pub fn reset(&mut self, default: &Layout<P>) {
|
||||
let layout = self.saved.clone().unwrap_or_else(|| default.clone());
|
||||
let shown: HashSet<P> = layout.panels().into_iter().cloned().collect();
|
||||
for panel in self.layout.panels() {
|
||||
if !shown.contains(panel)
|
||||
&& !self.hidden.iter().any(|(p, _)| p == panel)
|
||||
&& let Ok(location) = self.layout.location(panel)
|
||||
{
|
||||
self.hidden.push((panel.clone(), location));
|
||||
}
|
||||
}
|
||||
self.hidden.retain(|(p, _)| !shown.contains(p));
|
||||
self.layout = layout;
|
||||
}
|
||||
}
|
||||
|
||||
/// Where each of `closed` goes back, captured as if they closed last to first. Reopening them
|
||||
/// first to last rebuilds the group: the first restores the group itself, the rest join it.
|
||||
fn close_locations<P: Clone + Eq + Hash>(layout: &Layout<P>, closed: &[P]) -> Vec<(P, Location<P>)> {
|
||||
if let [panel] = closed {
|
||||
return layout.location(panel).map(|location| vec![(panel.clone(), location)]).unwrap_or_default();
|
||||
}
|
||||
let mut scratch = layout.clone();
|
||||
let mut locations = Vec::new();
|
||||
for panel in closed.iter().rev() {
|
||||
if let Ok(location) = scratch.location(panel) {
|
||||
locations.push((panel.clone(), location));
|
||||
}
|
||||
scratch.take(panel);
|
||||
}
|
||||
locations.reverse();
|
||||
locations
|
||||
}
|
||||
Reference in new issue
Block a user