mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 20:58:00 -06:00
* Build: craft-libs at 2869b8cc Pin craft-pdf, craft-print, craft-docpath, craft-ui, craft-keymap, craft-window and the patched hayro/krilla backends to craft-libs 2869b8cc1077530bd07a7e192fb2b0e1304a1354, with the pdf-deps gate, NOTICE, ASSETS.md and docs/pdf-backend.md at the same revision. PDF: no modification date beyond the creation date, DeviceN images are not JPEG-encoded, and clipping text draws nothing (its outlines arrive as a clip path). * Docking: compile against craft-ui at 2869b8cc Floating windows are trees of groups: window_panels and window_active read one as a flat group, and windows rebuilt from the legacy groups keep their ids. Unknown dock actions are rejected, the new ones are checked like the old, and every panel a close action removes keeps its return location. * Docking: panels and canvas on craft-ui's docking area * Docking: tests for the dock area, flyouts, floating panels and the Tools panel * Docking: catalogs without the old dock's strings * Build: craft-libs at 8a503cfe * Docking: floating panels in OS windows of their own on the desktop * Docking: modal dialogs block panel windows; their keys and dropped files reach the main window * Docking: art dragged onto a panel in its own window drops there * Docking: module doc names the OS windows * Build: craft-libs at a042cf26 * Docking: onto, above, below, collapsed, icon and column places on Float and Dock Panel; ui.docking * Docking: command descriptions of Float, Dock, Move and Collapse Panel for the craft-ui layout * Docking: control protocol describes the craft-ui layout, its commands and ui.docking * Docking: rustfmt * Docking: layouts saved before the canvas joined convert on load Saved ui.json files and workspaces from before the shared layout held the canvas (a dock-only tree plus floating_panels, docking_icons, docking_collapsed, dock_collapsed and dock_width) convert into one layout: the dock tree's groups in the main column, the icon column with the panels dropped on it, floating groups and sets, sets docked as columns, collapsed groups, the dock width and the places of closed panels. The legacy fields are read, never written. A ui.json read only in part is copied to ui.unreadable.json before anything saves over it. * Docking: catalogs without the old collapse message * Docking: converted floating sets start at most 720 points tall * Docking: closed icon panels convert into the icon column; old floating rects, set heights and a copy of the file before conversion * Build: craft-libs at 27e60765 * Docking: dock column places and icon count from the icon column; a docked column moves to a later place * Build: craft-libs at d24e4bcc * Build: craft-libs at 5bbe9f62 * Docs: floating panels in OS windows of their own close the other-monitor gap * Docking: keys forwarded from a floating panel's OS window run shortcuts while the main window is unfocused
151 lines
6.4 KiB
Rust
151 lines
6.4 KiB
Rust
//! The Tools panel floats by its title bar and docks again on the window's left edge
|
|
//! ([`crate::toolbar`]). It stays outside the docking layout: dragging its title bar out of the
|
|
//! docked panel floats it under the pointer, a floating panel's title bar moves it, and a release
|
|
//! over the strip along the window's left edge (lit up) docks it there. Escape or losing the
|
|
//! window's focus puts it back where the drag began.
|
|
//!
|
|
//! `window.panel.float {panel: "tools"}` / `window.panel.dock {panel: "tools"}` do the same for
|
|
//! agents. Its position is kept with the preferences and in user workspaces, and a position saved
|
|
//! on a bigger window is clamped into this one when drawn.
|
|
|
|
use egui::{CornerRadius, Id, Pos2, Rect, Stroke, Vec2, pos2, vec2};
|
|
use serde_json::{Value, json};
|
|
|
|
use crate::VectorcraftApp;
|
|
use crate::theme::Tokens;
|
|
|
|
/// Where `window.panel.float {panel: "tools"}` floats the Tools panel given no position.
|
|
const TOOLS_AT: [f32; 2] = [60.0, 120.0];
|
|
/// Points the pointer travels from its press before a drop counts (a click on the title bar of a
|
|
/// panel floating over the left edge doesn't dock it).
|
|
const TRAVEL: f32 = 4.0;
|
|
|
|
/// A move in progress: where the pointer holds the panel from its top-left corner, where the press
|
|
/// began, and where the panel was before it (to put it back on Escape).
|
|
#[derive(Clone, Debug)]
|
|
struct Move {
|
|
grab: Vec2,
|
|
from: Pos2,
|
|
original: Option<[f32; 2]>,
|
|
}
|
|
|
|
fn move_id() -> Id {
|
|
Id::unique("tools-float-move")
|
|
}
|
|
|
|
/// The strip along the window's left edge where the Tools panel docks, as last laid out.
|
|
pub(crate) fn tools_zone_id() -> Id {
|
|
Id::unique("tools-float-zone")
|
|
}
|
|
|
|
/// `pos` moved so a panel of `size` stays inside `screen` (a position saved on a bigger window).
|
|
pub fn clamp(pos: [f32; 2], size: Vec2, screen: Rect) -> Pos2 {
|
|
let [x, y] = pos;
|
|
let (x, y) = if x.is_finite() && y.is_finite() { (x, y) } else { (screen.left() + 60.0, screen.top() + 100.0) };
|
|
let fit = |v: f32, max: f32| if v > max { max.floor() } else { v };
|
|
pos2(fit(x, screen.right() - size.x).max(screen.left()), fit(y, screen.bottom() - size.y).max(screen.top()))
|
|
}
|
|
|
|
fn gesture_allowed(ctx: &egui::Context) -> bool {
|
|
ctx.input(|input| input.focused && !input.key_pressed(egui::Key::Escape))
|
|
}
|
|
|
|
fn start(app: &VectorcraftApp, ctx: &egui::Context, grab: Vec2) {
|
|
let Some(from) = ctx.input(|i| i.pointer.press_origin().or(i.pointer.interact_pos())) else { return };
|
|
ctx.data_mut(|d| d.insert_temp(move_id(), Move { grab, from, original: app.ui.toolbar_pos }));
|
|
}
|
|
|
|
/// The Tools panel's title bar was pressed (`title`, the panel's bounds `bounds`): docked, dragging
|
|
/// it out of the panel floats the panel under the pointer; floating at `floating`, a drag moves it.
|
|
pub(crate) fn drag_tools(app: &mut VectorcraftApp, ctx: &egui::Context, title: &egui::Response, bounds: Rect, floating: Option<Pos2>) {
|
|
if !gesture_allowed(ctx) || ctx.data(|d| d.get_temp::<Move>(move_id()).is_some()) {
|
|
return;
|
|
}
|
|
let Some(at) = ctx.input(|i| i.pointer.interact_pos()) else { return };
|
|
match floating {
|
|
Some(pos) if title.drag_started() => start(app, ctx, at - pos),
|
|
None if title.dragged() && !bounds.contains(at) => {
|
|
// Held where the press grabbed the title bar.
|
|
let grab = ctx.input(|i| i.pointer.press_origin()).map_or(vec2(10.0, 7.0), |p| p - bounds.min);
|
|
start(app, ctx, grab);
|
|
let p = at - grab;
|
|
app.ui.toolbar_pos = Some([p.x, p.y]);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
/// The docking strip under `at`, if any.
|
|
fn drop_zone(ctx: &egui::Context, at: Pos2) -> Option<Rect> {
|
|
ctx.data(|d| d.get_temp::<Rect>(tools_zone_id())).filter(|r| r.contains(at))
|
|
}
|
|
|
|
fn highlight(ctx: &egui::Context, zone: Rect) {
|
|
let t = Tokens::get(ctx);
|
|
let painter = ctx.layer_painter(egui::LayerId::new(egui::Order::Foreground, Id::unique("tools-float-drop")));
|
|
painter.rect(zone.shrink(1.0), CornerRadius::same(2), t.accent.gamma_multiply(0.2), Stroke::new(2.0, t.accent), egui::StrokeKind::Inside);
|
|
}
|
|
|
|
/// Carry a move on (at the start of a frame, before the panels are drawn): the panel follows the
|
|
/// pointer and the docking strip lights up under it; the release docks it there or leaves it
|
|
/// floating where it is.
|
|
pub fn track(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
|
let Some(m) = ctx.data(|d| d.get_temp::<Move>(move_id())) else { return };
|
|
if !gesture_allowed(ctx) {
|
|
app.ui.toolbar_pos = m.original;
|
|
ctx.data_mut(|d| d.remove::<Move>(move_id()));
|
|
return;
|
|
}
|
|
let at = ctx.input(|i| i.pointer.interact_pos());
|
|
let zone = at.filter(|at| at.distance(m.from) > TRAVEL).and_then(|at| drop_zone(ctx, at));
|
|
if ctx.input(|i| i.pointer.primary_down()) {
|
|
if let Some(p) = at.map(|at| at - m.grab)
|
|
&& p.x.is_finite()
|
|
&& p.y.is_finite()
|
|
&& app.ui.toolbar_pos.is_some()
|
|
{
|
|
app.ui.toolbar_pos = Some([p.x, p.y]);
|
|
}
|
|
if let Some(zone) = zone {
|
|
highlight(ctx, zone);
|
|
}
|
|
return;
|
|
}
|
|
ctx.data_mut(|d| d.remove::<Move>(move_id()));
|
|
if zone.is_some() {
|
|
app.ui.toolbar_pos = None;
|
|
}
|
|
}
|
|
|
|
/// The `x`, `y` params: a top-left corner in screen points, if given.
|
|
fn position(p: &Value) -> Result<Option<Pos2>, String> {
|
|
let coord = |k: &str| match p.get(k) {
|
|
None | Some(Value::Null) => Some(None),
|
|
Some(v) => v.as_f64().filter(|f| f.abs() < 1e6).map(|f| Some(f as f32)),
|
|
};
|
|
match (coord("x"), coord("y")) {
|
|
(Some(Some(x)), Some(Some(y))) => Ok(Some(pos2(x, y))),
|
|
(Some(None), Some(None)) => Ok(None),
|
|
_ => Err("x and y must both be numbers".into()),
|
|
}
|
|
}
|
|
|
|
/// `window.panel.float` / `window.panel.dock` for the Tools panel (`panel: "tools"`); `None` for
|
|
/// any other panel.
|
|
pub(crate) fn command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result<Value, String>> {
|
|
let float = match id {
|
|
"window.panel.float" => true,
|
|
"window.panel.dock" => false,
|
|
_ => return None,
|
|
};
|
|
let raw = p.get("panel").and_then(Value::as_str)?.trim();
|
|
if !(raw.eq_ignore_ascii_case("tools") || raw.eq_ignore_ascii_case("toolbar")) {
|
|
return None;
|
|
}
|
|
Some(position(p).map(|at| {
|
|
app.ui.toolbar_pos = float.then(|| at.map_or(TOOLS_AT, |p| [p.x, p.y]));
|
|
app.ui.toolbar |= float;
|
|
json!({ "panel": "tools", "floating": float, "pos": app.ui.toolbar_pos })
|
|
}))
|
|
}
|