mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 09:59:47 -06:00
Window: drag a document tab out to float it, and a window back to dock it
A document tab dragged more than 24 pt above or below the tab strip floats its document in a window under the pointer and makes it the current document (window.arrange.floatInWindow takes index and at for this). A shorter drag, or one along the strip, leaves the tabs as they are. A document window dragged by its title bar onto the tab strip goes back under the tabs; the strip shows an outline while a window is over it. A desktop document window is an OS window, so it has the document's tab at its top: dragging that tab moves the window, and dropping it on the main window's tab strip docks it (window.arrange.consolidateAllWindows takes index for one document). A document floated again opens where it is asked to, not where its last window was.
This commit is contained in:
@@ -433,6 +433,7 @@ impl VectorcraftApp {
|
||||
pub(crate) fn canvas_area(&mut self, ui: &mut egui::Ui) {
|
||||
let t = theme::Tokens::get(ui.ctx());
|
||||
ui.painter().rect_filled(ui.max_rect(), 0.0, t.pasteboard);
|
||||
self.canvas.windows.strip = None;
|
||||
if self.ui.screen_mode < 3 {
|
||||
chrome::doc_tabs(self, ui);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ use status_bar::{StatusReadoutWidths, status_readouts};
|
||||
#[cfg(test)]
|
||||
use tabs::{CLOSE_BEFORE_TITLE, first_tab_shown, tab_title};
|
||||
pub use tabs::{doc_tabs, isolation_bar, step_document};
|
||||
pub(crate) use tabs::{document_window_title, show_document};
|
||||
pub(crate) use tabs::{document_window_title, show_document, window_tab};
|
||||
|
||||
/// The application bar's height, in points (compact, as in PhotoCraft).
|
||||
pub const APP_BAR_HEIGHT: f32 = 32.0;
|
||||
|
||||
@@ -25,6 +25,51 @@ pub(crate) fn document_window_title(app: &VectorcraftApp, i: usize) -> Option<St
|
||||
Some(doc_tab_title(app, i, d))
|
||||
}
|
||||
|
||||
/// The height of the tab strip and the size of a tab's title, in points: larger with User
|
||||
/// Interface › Large Tabs.
|
||||
fn tab_size(app: &VectorcraftApp) -> (f32, f32) {
|
||||
if app.session.prefs.large_tabs { (44.0, 14.0) } else { (35.0, 12.5) }
|
||||
}
|
||||
|
||||
/// The tab at the top of document `i`'s floating OS window ([`crate::doc_windows`]): a strip with
|
||||
/// the document's tab, as the main window's strip draws it but without its ×. → the tab's
|
||||
/// response, which senses clicks and drags.
|
||||
pub(crate) fn window_tab(app: &VectorcraftApp, ui: &mut Ui, i: usize) -> egui::Response {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (height, title_size) = tab_size(app);
|
||||
let (strip, _) = ui.allocate_exact_size(vec2(ui.available_width(), height), Sense::hover());
|
||||
ui.painter().rect_filled(strip, 0.0, t.tab_strip);
|
||||
ui.painter().line_segment([strip.left_bottom(), strip.right_bottom()], Stroke::new(1.0, t.border));
|
||||
let title = document_window_title(app, i).unwrap_or_default();
|
||||
let galley = ui.painter().layout_no_wrap(title, theme::semibold(title_size), t.text_strong);
|
||||
let r = egui::Rect::from_min_size(strip.min, vec2((galley.size().x + 28.0).min(strip.width()), strip.height() - 1.0));
|
||||
ui.painter().rect_filled(r, 0.0, t.panel);
|
||||
ui.painter().line_segment([r.right_top(), r.right_bottom()], Stroke::new(1.5, t.border));
|
||||
ui.painter().galley(egui::pos2(r.left() + 14.0, r.center().y - galley.size().y / 2.0), galley, t.text);
|
||||
ui.interact(r, ui.scope_id().with("window-tab"), Sense::click_and_drag())
|
||||
}
|
||||
|
||||
/// While document tab `r` is dragged off the tab `strip` far enough to float its document, an
|
||||
/// outline under the pointer where its window will open; → the window's top-left corner when the
|
||||
/// drag ends there.
|
||||
fn tear_off(ui: &Ui, resp: &egui::Response, strip: egui::Rect) -> Option<egui::Pos2> {
|
||||
use crate::doc_windows::{GRAB, torn_off};
|
||||
if resp.dragged()
|
||||
&& let Some(p) = resp.interact_pointer_pos().filter(|p| torn_off(strip, *p))
|
||||
{
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let ghost = egui::Rect::from_min_size(p - GRAB, vec2(240.0, 160.0));
|
||||
let painter = ui.ctx().layer_painter(egui::LayerId::new(egui::Order::Tooltip, ui.scope_id().with("tab-tear-off")));
|
||||
painter.rect_filled(ghost, 0.0, t.accent.gamma_multiply(0.12));
|
||||
painter.rect_stroke(ghost, 0.0, Stroke::new(1.5, t.accent), egui::StrokeKind::Inside);
|
||||
}
|
||||
if !resp.drag_stopped() {
|
||||
return None;
|
||||
}
|
||||
let p = resp.interact_pointer_pos().or_else(|| ui.input(|i| i.pointer.latest_pos()))?;
|
||||
torn_off(strip, p).then_some(p - GRAB)
|
||||
}
|
||||
|
||||
/// Whether a document tab's × comes before its title (macOS) rather than after it (Windows,
|
||||
/// Linux and the web).
|
||||
pub(super) const CLOSE_BEFORE_TITLE: bool = cfg!(target_os = "macos");
|
||||
@@ -44,13 +89,16 @@ pub(super) fn first_tab_shown(widths: &[f32], active: Option<usize>, room: f32)
|
||||
/// taller, with larger titles. Tabs that don't fit are reached from a » button at the strip's
|
||||
/// right end, which lists every open document, and the active tab is always in view (#746).
|
||||
/// Documents in floating windows have no tab ([`crate::doc_windows`]); while the current document
|
||||
/// is in one, the tab of the document the strip shows is the highlighted one.
|
||||
/// is in one, the tab of the document the strip shows is the highlighted one. A tab dragged more
|
||||
/// than [`crate::doc_windows::TEAR_OFF`] above or below the strip floats its document in a window
|
||||
/// under the pointer.
|
||||
pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
/// The width of the » button.
|
||||
const MORE: f32 = 30.0;
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (height, title_size) = if app.session.prefs.large_tabs { (44.0, 14.0) } else { (35.0, 12.5) };
|
||||
let (height, title_size) = tab_size(app);
|
||||
let (strip, _) = ui.allocate_exact_size(vec2(ui.available_width(), height), Sense::hover());
|
||||
app.canvas.windows.strip = Some(strip);
|
||||
ui.painter().rect_filled(strip, 0.0, t.tab_strip);
|
||||
ui.painter().line_segment([strip.left_bottom(), strip.right_bottom()], Stroke::new(1.0, t.border));
|
||||
// On the Home screen no tab is the current one.
|
||||
@@ -74,6 +122,7 @@ pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let mut x = strip.left();
|
||||
let mut activate = None;
|
||||
let mut close = None;
|
||||
let mut float = None;
|
||||
let mut hovering_art = false;
|
||||
for (k, ((_, galley), w)) in tabs.iter().zip(&widths).enumerate().skip(first) {
|
||||
if x + w > strip.left() + room && Some(k) != active {
|
||||
@@ -82,7 +131,7 @@ pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let Some(&i) = docked.get(k) else { break };
|
||||
let is_active = Some(k) == active;
|
||||
let r = egui::Rect::from_min_size(egui::pos2(x, strip.top()), vec2(*w, strip.height() - 1.0));
|
||||
let resp = ui.interact(r, ui.scope_id().with(("tab", i)), Sense::click());
|
||||
let resp = ui.interact(r, ui.scope_id().with(("tab", i)), Sense::click_and_drag());
|
||||
if is_active {
|
||||
ui.painter().rect_filled(r, 0.0, t.panel);
|
||||
} else if resp.hovered() {
|
||||
@@ -101,6 +150,8 @@ pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
close = Some(i);
|
||||
} else if resp.clicked() {
|
||||
activate = Some(i);
|
||||
} else if let Some(at) = tear_off(ui, &resp, strip) {
|
||||
float = Some((i, at));
|
||||
}
|
||||
x += w;
|
||||
}
|
||||
@@ -131,6 +182,10 @@ pub fn doc_tabs(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
if let Err(e) = crate::unsaved::close(app, i) {
|
||||
app.status(e);
|
||||
}
|
||||
} else if let Some((i, at)) = float {
|
||||
if let Err(e) = app.run("window.arrange.floatInWindow", json!({"index": i, "at": [at.x, at.y]})) {
|
||||
app.status(e);
|
||||
}
|
||||
} else if let Some(i) = activate {
|
||||
show_document(app, i);
|
||||
}
|
||||
|
||||
@@ -5,11 +5,21 @@
|
||||
//! ([`crate::panel_docking`]). The current document's window holds the canvas
|
||||
//! ([`crate::canvas::show`]); each other window draws its document as a Tile pane does
|
||||
//! ([`crate::canvas::pane`]), and a press in it makes its document the current one.
|
||||
//!
|
||||
//! A document tab dragged off the tab strip floats its document in a window under the pointer,
|
||||
//! and a document window dragged onto the tab strip by its title bar (an OS window by the tab at
|
||||
//! its top) goes back under the tabs.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::VectorcraftApp;
|
||||
|
||||
/// How far above or below the tab strip, in points, a dragged document tab must be released to
|
||||
/// float its document.
|
||||
pub(crate) const TEAR_OFF: f32 = 24.0;
|
||||
/// Where a torn-off document's window opens: this far up and left of the pointer, so the pointer
|
||||
/// is over its title bar.
|
||||
pub(crate) const GRAB: egui::Vec2 = egui::vec2(40.0, 12.0);
|
||||
/// How far apart Cascade sets the windows, down and to the right, in points.
|
||||
pub(crate) const CASCADE_STEP: f32 = 30.0;
|
||||
/// How far the first cascaded window is from the document area's top-left corner, in points.
|
||||
@@ -24,6 +34,9 @@ pub struct DocWindow {
|
||||
pub uid: u64,
|
||||
/// Where the window goes, in the main window's points.
|
||||
pub place: egui::Rect,
|
||||
/// The window's own number ([`Windows::opened`]): a document floated again gets a new window,
|
||||
/// which opens where it is asked to, not where its last window was.
|
||||
number: u32,
|
||||
/// Counts the moves Cascade gives the window after it opened.
|
||||
generation: u32,
|
||||
/// The OS window's position on the screen when it opened or last moved, and the generation of
|
||||
@@ -36,6 +49,9 @@ pub struct DocWindow {
|
||||
/// The size of the body its document was last fitted to, and the zoom and center that fit
|
||||
/// gave: a window resized while its view is still there fits its document again.
|
||||
fit: Option<Fit>,
|
||||
/// While its OS window's tab is dragged: where the pointer is from the window's top-left
|
||||
/// corner, on the screen.
|
||||
grab: Option<egui::Vec2>,
|
||||
}
|
||||
|
||||
/// [`DocWindow::fit`]: a body size, and a view's zoom and center.
|
||||
@@ -43,7 +59,7 @@ type Fit = (egui::Vec2, f64, vectorcraft_geom::Point);
|
||||
|
||||
impl DocWindow {
|
||||
fn new(uid: u64, place: egui::Rect) -> Self {
|
||||
Self { uid, place, generation: 0, screen: None, focused: false, body: None, fit: None }
|
||||
Self { uid, place, number: 0, generation: 0, screen: None, focused: false, body: None, fit: None, grab: None }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +73,15 @@ pub struct Windows {
|
||||
current: Option<u64>,
|
||||
/// While Presentation Mode is on: the floating documents (their uids) it has shown.
|
||||
presented: Option<Vec<u64>>,
|
||||
/// The main window's tab strip in its last frame, while it was drawn.
|
||||
pub(crate) strip: Option<egui::Rect>,
|
||||
/// Counts the windows opened ([`DocWindow::number`]).
|
||||
opened: u32,
|
||||
}
|
||||
|
||||
/// Whether a document tab dragged from `strip` to `p` has left it far enough to float.
|
||||
pub(crate) fn torn_off(strip: egui::Rect, p: egui::Pos2) -> bool {
|
||||
p.y > strip.bottom() + TEAR_OFF || p.y < strip.top() - TEAR_OFF
|
||||
}
|
||||
|
||||
/// Whether document `i` is in a floating window.
|
||||
@@ -125,7 +150,10 @@ fn float(app: &mut VectorcraftApp, docs: &[usize]) {
|
||||
continue;
|
||||
}
|
||||
let place = places.get(first + k).copied().unwrap_or(egui::Rect::from_min_size(egui::Pos2::ZERO, MIN_SIZE));
|
||||
app.ui.floating.push(DocWindow::new(uid, place));
|
||||
app.canvas.windows.opened = app.canvas.windows.opened.wrapping_add(1);
|
||||
let mut w = DocWindow::new(uid, place);
|
||||
w.number = app.canvas.windows.opened;
|
||||
app.ui.floating.push(w);
|
||||
if let Some(v) = app.views.get_mut(*i) {
|
||||
v.fitted = false;
|
||||
}
|
||||
@@ -140,15 +168,60 @@ fn floating_indices(app: &VectorcraftApp) -> Vec<usize> {
|
||||
app.ui.floating.iter().filter_map(|w| app.session.documents().iter().position(|d| d.uid == w.uid)).collect()
|
||||
}
|
||||
|
||||
/// `window.arrange.floatInWindow`: the current document leaves the tab strip for a floating window
|
||||
/// of its own. A document already floating stays where it is. Returns `{floating}`.
|
||||
pub(crate) fn float_in_window(app: &mut VectorcraftApp) -> Result<Value, String> {
|
||||
if let Some(i) = app.session.active_index().filter(|i| !is_floating(app, *i)) {
|
||||
/// The open document `p`'s `index` names: `Ok(None)` without one.
|
||||
fn document_param(app: &VectorcraftApp, p: &Value) -> Result<Option<usize>, String> {
|
||||
match p.get("index") {
|
||||
None | Some(Value::Null) => Ok(None),
|
||||
Some(v) => v
|
||||
.as_u64()
|
||||
.and_then(|i| usize::try_from(i).ok())
|
||||
.filter(|i| *i < app.session.documents().len())
|
||||
.map(Some)
|
||||
.ok_or_else(|| "no such document".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// `window.arrange.floatInWindow`: the current document, or document `index`, leaves the tab strip
|
||||
/// for a floating window of its own, with its top-left corner at `at` (`[x, y]` in the main
|
||||
/// window's points) when given, else at the next cascade place. A document already floating stays
|
||||
/// where it is. Document `index` becomes the current one. Returns `{floating}`.
|
||||
pub(crate) fn float_in_window(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
|
||||
let index = document_param(app, p)?;
|
||||
let at = p
|
||||
.get("at")
|
||||
.and_then(Value::as_array)
|
||||
.and_then(|a| Some(egui::pos2(a.first()?.as_f64()? as f32, a.get(1)?.as_f64()? as f32)))
|
||||
.filter(|at| at.is_finite());
|
||||
if let Some(i) = index.or(app.session.active_index()).filter(|i| !is_floating(app, *i)) {
|
||||
float(app, &[i]);
|
||||
if let (Some(at), Some(w)) = (at, app.ui.floating.last_mut()) {
|
||||
w.place = egui::Rect::from_min_size(at, w.place.size());
|
||||
}
|
||||
}
|
||||
if let Some(i) = index {
|
||||
crate::chrome::show_document(app, i);
|
||||
}
|
||||
Ok(json!({ "floating": floating_indices(app) }))
|
||||
}
|
||||
|
||||
/// `window.arrange.consolidateAllWindows` with an `index`: document `index`, floating or not,
|
||||
/// goes back under its tab, fitted there, and becomes the current one; the other windows and Tile
|
||||
/// stay as they are. Returns `{tiled, floating}`.
|
||||
pub(crate) fn dock(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
|
||||
let Some(i) = document_param(app, p)? else {
|
||||
return Err("no such document".into());
|
||||
};
|
||||
if is_floating(app, i) {
|
||||
let uid = app.session.documents().get(i).map(|d| d.uid);
|
||||
app.ui.floating.retain(|w| Some(w.uid) != uid);
|
||||
if let Some(v) = app.views.get_mut(i) {
|
||||
v.fitted = false;
|
||||
}
|
||||
}
|
||||
crate::chrome::show_document(app, i);
|
||||
Ok(json!({ "tiled": app.ui.tiled, "floating": floating_indices(app) }))
|
||||
}
|
||||
|
||||
/// `window.arrange.floatAllInWindows`: every document under the tab strip goes to a floating window
|
||||
/// of its own. Returns `{floating}`.
|
||||
pub(crate) fn float_all(app: &mut VectorcraftApp) -> Result<Value, String> {
|
||||
@@ -216,10 +289,10 @@ fn viewport_id(uid: u64) -> egui::ViewportId {
|
||||
egui::ViewportId::from_hash_of(("vectorcraft-document-window", uid))
|
||||
}
|
||||
|
||||
/// The id of document window `w` inside the main window: a new one for each Cascade move, so egui
|
||||
/// opens it at its new place and size.
|
||||
/// The id of document window `w` inside the main window: a new one for each window and each
|
||||
/// Cascade move, so egui opens it at its new place and size.
|
||||
fn window_id(w: &DocWindow) -> egui::Id {
|
||||
egui::Id::unique(("vectorcraft-document-window", w.uid, w.generation))
|
||||
egui::Id::unique(("vectorcraft-document-window", w.uid, w.number, w.generation))
|
||||
}
|
||||
|
||||
/// What happened in a document window this frame.
|
||||
@@ -229,6 +302,10 @@ struct Shown {
|
||||
close: bool,
|
||||
/// Its document asks to become the current one.
|
||||
activate: bool,
|
||||
/// It is being dragged over the main window's tab strip.
|
||||
over_strip: bool,
|
||||
/// It was dropped on the main window's tab strip: its document goes back under the tabs.
|
||||
dock: bool,
|
||||
/// Its OS window has the keyboard focus.
|
||||
focused: bool,
|
||||
/// Where it drew its document.
|
||||
@@ -297,6 +374,8 @@ pub(crate) fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let blocked = crate::panel_docking::modal_open(app, ctx);
|
||||
let mut close = None;
|
||||
let mut activate = None;
|
||||
let mut dock = None;
|
||||
let mut over_strip = false;
|
||||
for w in app.ui.floating.clone() {
|
||||
let Some(i) = app.session.documents().iter().position(|d| d.uid == w.uid) else { continue };
|
||||
let shown = if native {
|
||||
@@ -316,12 +395,22 @@ pub(crate) fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
win.body = shown.body;
|
||||
win.fit = shown.fit;
|
||||
}
|
||||
over_strip |= shown.over_strip;
|
||||
if shown.close {
|
||||
close = Some(i);
|
||||
} else if shown.dock {
|
||||
dock = Some(i);
|
||||
} else if shown.activate && app.session.active_index() != Some(i) {
|
||||
activate = Some(i);
|
||||
}
|
||||
}
|
||||
// The tab strip a dragged window would go back under shows where it lands.
|
||||
if let Some(strip) = app.canvas.windows.strip.filter(|_| over_strip) {
|
||||
let t = crate::theme::Tokens::get(ctx);
|
||||
let painter = ctx.layer_painter(egui::LayerId::new(egui::Order::Tooltip, egui::Id::unique("vectorcraft-document-dock")));
|
||||
painter.rect_filled(strip, 0.0, t.accent.gamma_multiply(0.12));
|
||||
painter.rect_stroke(strip, 0.0, egui::Stroke::new(2.0, t.accent), egui::StrokeKind::Inside);
|
||||
}
|
||||
if let Some(i) = close {
|
||||
// The save prompt opens in the main window.
|
||||
if let Err(e) = crate::unsaved::close(app, i) {
|
||||
@@ -330,6 +419,13 @@ pub(crate) fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
if native && app.ui.dialog.is_some() {
|
||||
ctx.send_viewport_cmd_to(egui::ViewportId::ROOT, egui::ViewportCommand::Focus);
|
||||
}
|
||||
} else if let Some(i) = dock {
|
||||
if let Err(e) = app.run("window.arrange.consolidateAllWindows", json!({ "index": i })) {
|
||||
app.status(e);
|
||||
}
|
||||
if native {
|
||||
ctx.send_viewport_cmd_to(egui::ViewportId::ROOT, egui::ViewportCommand::Focus);
|
||||
}
|
||||
} else if let Some(i) = activate {
|
||||
crate::chrome::show_document(app, i);
|
||||
app.canvas.windows.current = app.session.active().map(|d| d.uid);
|
||||
@@ -367,7 +463,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, (i, blocked): (usize, bool)
|
||||
}
|
||||
|
||||
/// Document `i`'s window `w` as an OS window of its own, `blocked` by a modal or not; `raise`
|
||||
/// brings it to the front.
|
||||
/// brings it to the front. The document's tab is at the top of the window, above its body.
|
||||
fn os_window(app: &mut VectorcraftApp, ctx: &egui::Context, w: &DocWindow, (i, blocked): (usize, bool), raise: bool) -> Shown {
|
||||
let vid = viewport_id(w.uid);
|
||||
let origin = ctx.input(|inp| inp.viewport().inner_rect.map(|r| r.min)).filter(|p| p.is_finite()).unwrap_or(egui::Pos2::ZERO);
|
||||
@@ -392,6 +488,8 @@ fn os_window(app: &mut VectorcraftApp, ctx: &egui::Context, w: &DocWindow, (i, b
|
||||
let builder =
|
||||
egui::ViewportBuilder::default().with_title(title).with_position(screen).with_inner_size(w.place.size()).with_min_inner_size(MIN_SIZE);
|
||||
let mut forwarded = (Vec::new(), None);
|
||||
// The main window's tab strip on the screen.
|
||||
let strip = app.canvas.windows.strip.map(|s| s.translate(origin.to_vec2()));
|
||||
let shown = ctx.show_viewport_immediate(vid, builder, |ui, _| {
|
||||
let (close, focused) = ui.input(|inp| (inp.viewport().close_requested(), inp.viewport().focused == Some(true)));
|
||||
if close {
|
||||
@@ -399,7 +497,10 @@ fn os_window(app: &mut VectorcraftApp, ctx: &egui::Context, w: &DocWindow, (i, b
|
||||
// or a modal can keep the document open.
|
||||
ui.ctx().send_viewport_cmd(egui::ViewportCommand::CancelClose);
|
||||
}
|
||||
let (activate, rect, fit) = body(app, ui, (i, blocked), w.fit);
|
||||
let tab = crate::chrome::window_tab(app, ui, i);
|
||||
let (over_strip, dock) = if blocked { (false, false) } else { drag_tab(app, ui, (w.uid, origin), &tab, strip) };
|
||||
let (pressed, rect, fit) = body(app, ui, (i, blocked), w.fit);
|
||||
let activate = pressed || (!blocked && (tab.clicked() || tab.drag_started()));
|
||||
// Keys the window's widgets left over run the main window's shortcuts and tool keys, as a
|
||||
// floating panel's do.
|
||||
if !blocked && !ui.memory(|m| m.focused().is_some()) {
|
||||
@@ -415,7 +516,7 @@ fn os_window(app: &mut VectorcraftApp, ctx: &egui::Context, w: &DocWindow, (i, b
|
||||
(events, Some(inp.modifiers))
|
||||
});
|
||||
}
|
||||
Shown { close, activate, focused, body: Some(rect), fit }
|
||||
Shown { close, activate, over_strip, dock, focused, body: Some(rect), fit }
|
||||
});
|
||||
if !forwarded.0.is_empty() {
|
||||
crate::panel_docking::forward_keys(app, ctx, forwarded.0, forwarded.1);
|
||||
@@ -423,6 +524,45 @@ fn os_window(app: &mut VectorcraftApp, ctx: &egui::Context, w: &DocWindow, (i, b
|
||||
shown
|
||||
}
|
||||
|
||||
/// A drag of document window `uid`'s OS window by the `tab` at its top: the window follows the
|
||||
/// pointer, and its document goes back under the tabs when the tab is released over the main
|
||||
/// window's tab `strip` (on the screen). `origin` is the main window's top-left corner on the
|
||||
/// screen. → whether the pointer is over the strip, and whether the tab was released there.
|
||||
fn drag_tab(
|
||||
app: &mut VectorcraftApp,
|
||||
ui: &egui::Ui,
|
||||
(uid, origin): (u64, egui::Pos2),
|
||||
tab: &egui::Response,
|
||||
strip: Option<egui::Rect>,
|
||||
) -> (bool, bool) {
|
||||
let Some(win) = app.ui.floating.iter_mut().find(|x| x.uid == uid) else {
|
||||
return (false, false);
|
||||
};
|
||||
if !tab.dragged() && !tab.drag_stopped() {
|
||||
win.grab = None;
|
||||
return (false, false);
|
||||
}
|
||||
let (inner, outer, pointer) = ui.input(|inp| (inp.viewport().inner_rect, inp.viewport().outer_rect, inp.pointer.latest_pos()));
|
||||
let (Some(inner), Some(outer), Some(pointer)) = (inner, outer, pointer) else {
|
||||
return (false, false);
|
||||
};
|
||||
let at = inner.min + pointer.to_vec2();
|
||||
// The pointer stays where it pressed the tab, relative to the window.
|
||||
let pressed = ui.input(|inp| inp.pointer.press_origin()).map_or(at, |p| inner.min + p.to_vec2());
|
||||
let grab = *win.grab.get_or_insert(pressed - outer.min);
|
||||
let over = strip.is_some_and(|s| s.contains(at));
|
||||
if tab.drag_stopped() {
|
||||
win.grab = None;
|
||||
return (over, over);
|
||||
}
|
||||
let pos = at - grab;
|
||||
if pos != outer.min {
|
||||
win.screen = Some((pos, win.generation));
|
||||
win.place = egui::Rect::from_min_size(pos - origin.to_vec2(), win.place.size());
|
||||
}
|
||||
(over, false)
|
||||
}
|
||||
|
||||
/// Document `i`'s window `w` as a window inside the main window, `blocked` by a modal or not;
|
||||
/// `raise` brings it to the front.
|
||||
fn app_window(app: &mut VectorcraftApp, ctx: &egui::Context, w: &DocWindow, (i, blocked): (usize, bool), raise: bool) -> Shown {
|
||||
@@ -449,8 +589,15 @@ fn app_window(app: &mut VectorcraftApp, ctx: &egui::Context, w: &DocWindow, (i,
|
||||
});
|
||||
// A press on its title bar or edges, which brings the window to the front, also makes its
|
||||
// document the current one.
|
||||
activate |= shown.is_some_and(|s| s.response.contains_pointer()) && ctx.input(|inp| inp.pointer.any_pressed());
|
||||
Shown { close: !open, activate, focused: false, body: rect, fit }
|
||||
activate |= shown.as_ref().is_some_and(|s| s.response.contains_pointer()) && ctx.input(|inp| inp.pointer.any_pressed());
|
||||
// Moved by its title bar onto the main window's tab strip, it goes back under the tabs.
|
||||
// egui moves a window by the drag of its title bar's own widget, or, where it moves windows by
|
||||
// a drag anywhere (after touch input), by the drag of the window itself.
|
||||
let mover = ctx.read_response(id.with("__title_click")).or_else(|| shown.as_ref().map(|s| s.response.clone()));
|
||||
let moved = mover.map_or((false, false), |r| (r.dragged(), r.drag_stopped()));
|
||||
let pointer = ctx.input(|inp| inp.pointer.latest_pos());
|
||||
let over_strip = !blocked && (moved.0 || moved.1) && app.canvas.windows.strip.zip(pointer).is_some_and(|(strip, p)| strip.contains(p));
|
||||
Shown { close: !open, activate, over_strip, dock: over_strip && moved.1, focused: false, body: rect, fit }
|
||||
}
|
||||
|
||||
/// The host's rewrites of raw input ([`crate::services::Services::native_input_hook`]) for the
|
||||
|
||||
@@ -60,7 +60,7 @@ fn body_of(app: &VectorcraftApp, i: usize) -> egui::Rect {
|
||||
fn window_of(app: &VectorcraftApp, ctx: &egui::Context, i: usize) -> egui::Rect {
|
||||
let uid = app.session.documents()[i].uid;
|
||||
let w = app.ui.floating.iter().find(|w| w.uid == uid).unwrap();
|
||||
ctx.memory(|m| m.area_rect(egui::Id::unique(("vectorcraft-document-window", w.uid, w.generation)))).unwrap()
|
||||
ctx.memory(|m| m.area_rect(window_id(w))).unwrap()
|
||||
}
|
||||
|
||||
/// A drag with the primary button from `from` to `to`, then a frame to settle.
|
||||
@@ -279,7 +279,7 @@ fn closing_a_floating_window_closes_its_document_and_asks_to_save_an_unsaved_one
|
||||
let close_button = |app: &VectorcraftApp, i: usize| {
|
||||
let uid = app.session.documents()[i].uid;
|
||||
let w = app.ui.floating.iter().find(|w| w.uid == uid).unwrap();
|
||||
let area = ctx.memory(|m| m.area_rect(egui::Id::unique(("vectorcraft-document-window", w.uid, w.generation)))).unwrap();
|
||||
let area = ctx.memory(|m| m.area_rect(window_id(w))).unwrap();
|
||||
let body = w.body.unwrap();
|
||||
(area, body)
|
||||
};
|
||||
@@ -363,7 +363,7 @@ fn a_document_made_current_in_presentation_mode_comes_to_the_front_after_it() {
|
||||
let layer = |app: &VectorcraftApp, i: usize| {
|
||||
let uid = app.session.documents()[i].uid;
|
||||
let w = app.ui.floating.iter().find(|w| w.uid == uid).unwrap();
|
||||
egui::LayerId::new(egui::Order::Middle, egui::Id::unique(("vectorcraft-document-window", w.uid, w.generation)))
|
||||
egui::LayerId::new(egui::Order::Middle, window_id(w))
|
||||
};
|
||||
let top = |ctx: &egui::Context| ctx.memory(|m| m.areas().top_layer_id(egui::Order::Middle));
|
||||
assert_eq!(top(&ctx), Some(layer(&app, 1)), "the current document's window is in front");
|
||||
@@ -378,11 +378,15 @@ fn a_document_made_current_in_presentation_mode_comes_to_the_front_after_it() {
|
||||
}
|
||||
|
||||
/// A stand-in for the window system of the documents' OS windows: it runs each one's frame, asks
|
||||
/// each to close while `close` is set, and records the commands the windows were sent.
|
||||
/// each to close while `close` is set, and records the commands the windows were sent. Each
|
||||
/// window is 500 × 400 points with its top-left corner at `origin` on the screen, and its next
|
||||
/// frame takes `events`.
|
||||
#[derive(Default)]
|
||||
struct Host {
|
||||
close: bool,
|
||||
commands: Vec<(egui::ViewportId, egui::ViewportCommand)>,
|
||||
origin: Pos2,
|
||||
events: Vec<egui::Event>,
|
||||
}
|
||||
|
||||
fn native_host() -> std::rc::Rc<std::cell::RefCell<Host>> {
|
||||
@@ -390,11 +394,25 @@ fn native_host() -> std::rc::Rc<std::cell::RefCell<Host>> {
|
||||
let shared = host.clone();
|
||||
egui::Context::set_immediate_viewport_renderer(move |ctx, mut viewport| {
|
||||
let id = viewport.ids.this;
|
||||
let events = if shared.borrow().close { vec![egui::ViewportEvent::Close] } else { vec![] };
|
||||
let rect = egui::Rect::from_min_size(Pos2::ZERO, vec2(500.0, 400.0));
|
||||
let info = egui::ViewportInfo { events, inner_rect: Some(rect), outer_rect: Some(rect), focused: Some(false), ..Default::default() };
|
||||
let input =
|
||||
egui::RawInput { viewport_id: id, viewports: std::iter::once((id, info)).collect(), screen_rect: Some(rect), ..Default::default() };
|
||||
let close = if shared.borrow().close { vec![egui::ViewportEvent::Close] } else { vec![] };
|
||||
let size = vec2(500.0, 400.0);
|
||||
let rect = egui::Rect::from_min_size(Pos2::ZERO, size);
|
||||
let on_screen = egui::Rect::from_min_size(shared.borrow().origin, size);
|
||||
let info = egui::ViewportInfo {
|
||||
events: close,
|
||||
inner_rect: Some(on_screen),
|
||||
outer_rect: Some(on_screen),
|
||||
focused: Some(false),
|
||||
..Default::default()
|
||||
};
|
||||
let events = std::mem::take(&mut shared.borrow_mut().events);
|
||||
let input = egui::RawInput {
|
||||
viewport_id: id,
|
||||
viewports: std::iter::once((id, info)).collect(),
|
||||
screen_rect: Some(rect),
|
||||
events,
|
||||
..Default::default()
|
||||
};
|
||||
let mut full = ctx.run_ui(input, |ui| (viewport.viewport_ui_cb)(ui));
|
||||
full.textures_delta.clear();
|
||||
let mut host = shared.borrow_mut();
|
||||
@@ -433,6 +451,188 @@ fn closing_an_os_window_keeps_it_while_its_unsaved_document_stays_open() {
|
||||
);
|
||||
}
|
||||
|
||||
/// Where the last frame drew the title of `title`'s tab in the tab strip.
|
||||
fn tab_at(shapes: &[Shape], title: &str) -> Pos2 {
|
||||
shapes
|
||||
.iter()
|
||||
.find_map(|s| match s {
|
||||
Shape::Text(t) if t.pos.y < 35.0 && t.galley.text().contains(title) => Some(t.pos + vec2(4.0, 4.0)),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tab_is_torn_off_only_well_above_or_below_the_strip() {
|
||||
let strip = egui::Rect::from_min_max(pos2(0.0, 40.0), pos2(1200.0, 75.0));
|
||||
assert!(!torn_off(strip, pos2(300.0, 60.0)), "along the strip");
|
||||
assert!(!torn_off(strip, pos2(300.0, 75.0 + TEAR_OFF - 1.0)), "a wobble below the strip");
|
||||
assert!(torn_off(strip, pos2(300.0, 75.0 + TEAR_OFF + 1.0)));
|
||||
assert!(torn_off(strip, pos2(300.0, 40.0 - TEAR_OFF - 1.0)), "dragged up");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dragging_a_tab_off_the_strip_floats_its_document_at_the_pointer() {
|
||||
let mut app = app_with(3);
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let shapes = frame(&mut app, &ctx, vec![]);
|
||||
let title = app.session.documents()[0].title();
|
||||
let tab = tab_at(&shapes, &title);
|
||||
// A short drag, and a drag along the strip, leave the tabs as they are.
|
||||
drag(&mut app, &ctx, tab, tab + vec2(0.0, 35.0 + TEAR_OFF - 10.0 - tab.y));
|
||||
drag(&mut app, &ctx, tab, tab + vec2(300.0, 0.0));
|
||||
assert!(app.ui.floating.is_empty(), "the document stays under its tab");
|
||||
assert_eq!(app.session.active_index(), Some(2), "a drag is not a click");
|
||||
// Dragged well below the strip, it floats in a window under the pointer and becomes current.
|
||||
let to = pos2(300.0, 200.0);
|
||||
drag(&mut app, &ctx, tab, to);
|
||||
assert_eq!(docked(&app), [1, 2]);
|
||||
assert_eq!(app.session.active_index(), Some(0), "the torn-off document is the current one");
|
||||
assert_eq!(app.ui.floating[0].place.min, to - GRAB, "its window opens under the pointer");
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let shapes = frame(&mut app, &ctx, vec![]);
|
||||
assert!(!tab_drawn(&shapes, &title), "it has left the strip");
|
||||
let window = window_of(&app, &ctx, 0);
|
||||
assert!(window.contains(to) && (window.min - (to - GRAB)).length() < 1.0, "the pointer is over its window: {window:?}");
|
||||
assert!(body_of(&app, 0).contains_rect(app.canvas_rect.unwrap()), "its window holds the canvas");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_window_dragged_by_its_title_bar_onto_the_strip_goes_back_under_the_tabs() {
|
||||
let mut app = app_with(3);
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
app.run("window.arrange.floatAllInWindows", json!({})).unwrap();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let title_bar = |app: &VectorcraftApp, i: usize| {
|
||||
let (window, body) = (window_of(app, &ctx, i), body_of(app, i));
|
||||
pos2(window.left() + 60.0, (window.top() + body.top()) / 2.0)
|
||||
};
|
||||
// Moved elsewhere, document 2's window (in front) stays a window.
|
||||
let from = title_bar(&app, 2);
|
||||
drag(&mut app, &ctx, from, from + vec2(40.0, 30.0));
|
||||
assert_eq!(app.ui.floating.len(), 3, "a window moved off the strip still floats");
|
||||
// Dropped on the strip, it goes back under its tab, current; the other windows stay.
|
||||
let from = title_bar(&app, 2);
|
||||
drag(&mut app, &ctx, from, pos2(600.0, 15.0));
|
||||
assert_eq!(docked(&app), [2]);
|
||||
assert_eq!(app.session.active_index(), Some(2));
|
||||
assert_eq!(app.ui.floating.len(), 2);
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let shapes = frame(&mut app, &ctx, vec![]);
|
||||
assert!(tab_drawn(&shapes, &app.session.documents()[2].title()), "it has its tab again");
|
||||
assert!(app.views[2].fitted, "it fits the canvas under the tabs");
|
||||
let canvas = app.canvas_rect.unwrap();
|
||||
assert!(canvas.top() >= 35.0 && canvas.width() > 1000.0, "the canvas under the tabs shows it: {canvas:?}");
|
||||
// Torn off again, it opens where its tab is dropped, not where its last window was.
|
||||
let to = pos2(200.0, 300.0);
|
||||
let tab = tab_at(&shapes, &app.session.documents()[2].title());
|
||||
drag(&mut app, &ctx, tab, to);
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let window = window_of(&app, &ctx, 2);
|
||||
assert!((window.min - (to - GRAB)).length() < 1.0, "its new window is under the pointer: {window:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn after_touch_input_a_window_dragged_onto_the_strip_still_docks() {
|
||||
let mut app = app_with(2);
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
app.run("window.arrange.floatAllInWindows", json!({})).unwrap();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
// A touch makes egui move windows by a drag anywhere on them.
|
||||
let touch = egui::Event::Touch {
|
||||
device_id: egui::TouchDeviceId(1),
|
||||
id: egui::TouchId(1),
|
||||
phase: egui::TouchPhase::Start,
|
||||
pos: pos2(1100.0, 700.0),
|
||||
force: None,
|
||||
};
|
||||
frame(&mut app, &ctx, vec![touch]);
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let (window, body) = (window_of(&app, &ctx, 1), body_of(&app, 1));
|
||||
let from = pos2(window.left() + 60.0, (window.top() + body.top()) / 2.0);
|
||||
drag(&mut app, &ctx, from, pos2(600.0, 15.0));
|
||||
assert_eq!(docked(&app), [1], "document 1 is back under the tabs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_document_floated_again_after_cascade_opens_where_it_is_asked_to() {
|
||||
let mut app = app_with(2);
|
||||
let ctx = egui::Context::default();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
app.run("window.arrange.floatAllInWindows", json!({})).unwrap();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
app.run("window.arrange.cascade", json!({})).unwrap();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
app.run("window.arrange.consolidateAllWindows", json!({"index": 1})).unwrap();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let at = pos2(200.0, 300.0);
|
||||
app.run("window.arrange.floatInWindow", json!({"index": 1, "at": [at.x, at.y]})).unwrap();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let window = window_of(&app, &ctx, 1);
|
||||
assert!((window.min - at).length() < 1.0, "its new window opens at {at:?}: {window:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn float_in_window_and_consolidate_take_a_document() {
|
||||
let mut app = app_with(3);
|
||||
assert_eq!(app.run("window.arrange.floatInWindow", json!({"index": 0, "at": [100, 120]})).unwrap(), json!({"floating": [0]}));
|
||||
assert_eq!(app.session.active_index(), Some(0), "document 0 becomes the current one");
|
||||
assert_eq!(app.ui.floating[0].place.min, pos2(100.0, 120.0));
|
||||
app.run("window.arrange.floatInWindow", json!({"index": 1})).unwrap();
|
||||
assert_eq!(app.run("window.arrange.floatInWindow", json!({"index": 3})), Err("no such document".to_string()));
|
||||
app.run("window.arrange.tile", json!({})).unwrap();
|
||||
app.run("window.arrange.floatAllInWindows", json!({})).unwrap();
|
||||
assert_eq!(app.run("window.arrange.consolidateAllWindows", json!({"index": 1})).unwrap(), json!({"tiled": true, "floating": [0, 2]}));
|
||||
assert_eq!(app.session.active_index(), Some(1));
|
||||
assert_eq!(docked(&app), [1], "only document 1 goes back under the tabs");
|
||||
assert_eq!(app.run("window.arrange.consolidateAllWindows", json!({"index": 7})), Err("no such document".to_string()));
|
||||
assert_eq!(app.run("window.arrange.consolidateAllWindows", json!({})).unwrap(), json!({"tiled": false, "floating": []}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_os_window_moves_by_its_tab_and_docks_on_the_strip() {
|
||||
let host = native_host();
|
||||
let ctx = egui::Context::default();
|
||||
ctx.set_embed_viewports(false);
|
||||
let mut app = app_with(2);
|
||||
if !native(&app, &ctx) {
|
||||
// Wayland: the windows are inside the main window.
|
||||
return;
|
||||
}
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
app.run("window.arrange.floatInWindow", json!({})).unwrap();
|
||||
// The OS window's top-left corner is at (300, 300) on the screen; the main window's at (0, 0).
|
||||
host.borrow_mut().origin = pos2(300.0, 300.0);
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let os_drag = |app: &mut VectorcraftApp, from: Pos2, to: Pos2| {
|
||||
let steps = [vec![egui::Event::PointerMoved(from)], vec![button(from, true)]]
|
||||
.into_iter()
|
||||
.chain((1..=4).map(|k| vec![egui::Event::PointerMoved(from + (to - from) * (k as f32 / 4.0))]))
|
||||
.chain([vec![button(to, false)], vec![]]);
|
||||
for events in steps {
|
||||
host.borrow_mut().events = events;
|
||||
frame(app, &ctx, vec![]);
|
||||
}
|
||||
};
|
||||
// The tab at the top of the window: dragged down and right, the window follows the pointer.
|
||||
let tab = pos2(20.0, 15.0);
|
||||
os_drag(&mut app, tab, tab + vec2(100.0, 50.0));
|
||||
assert!(is_floating(&app, 1), "a window moved off the strip still floats");
|
||||
let w = &app.ui.floating[0];
|
||||
assert_eq!(w.screen.map(|s| s.0), Some(pos2(400.0, 350.0)), "the window moved with the pointer");
|
||||
assert_eq!(w.place.min, pos2(400.0, 350.0), "in the main window's points");
|
||||
// Released over the main window's tab strip (y 0 to 35 on the screen), it docks.
|
||||
os_drag(&mut app, tab, pos2(20.0, -280.0));
|
||||
assert!(app.ui.floating.is_empty(), "the document is back under the tabs");
|
||||
assert_eq!(docked(&app), [0, 1]);
|
||||
assert_eq!(app.session.active_index(), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_arrange_items_are_enabled_as_the_documents_allow() {
|
||||
let ids = [
|
||||
|
||||
@@ -547,8 +547,9 @@ fn run_ui_part6(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Result<
|
||||
"window.newWindow" => Err("New Window isn't available yet: a document opens in one window".into()),
|
||||
"window.arrange.cascade" => crate::doc_windows::cascade(app),
|
||||
"window.arrange.tile" => crate::canvas::tile(app),
|
||||
"window.arrange.floatInWindow" => crate::doc_windows::float_in_window(app),
|
||||
"window.arrange.floatInWindow" => crate::doc_windows::float_in_window(app, p),
|
||||
"window.arrange.floatAllInWindows" => crate::doc_windows::float_all(app),
|
||||
"window.arrange.consolidateAllWindows" if p.get("index").is_some_and(|i| !i.is_null()) => crate::doc_windows::dock(app, p),
|
||||
"window.arrange.consolidateAllWindows" => crate::canvas::consolidate(app),
|
||||
"window.nextDocument" => crate::chrome::step_document(app, true),
|
||||
"window.previousDocument" => crate::chrome::step_document(app, false),
|
||||
|
||||
@@ -597,7 +597,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"window.arrange.floatInWindow",
|
||||
"Float in Window",
|
||||
"",
|
||||
"{} move the current document out of the tab strip into a floating window of its own, fitted to it; the window holding the current document takes the tools, a click in another document's window makes its document the current one; a document already floating stays where it is; returns {floating} (document indices)",
|
||||
"{index?, at?: [x, y]} move the current document, or document `index` (which becomes the current one), out of the tab strip into a floating window of its own, fitted to it, with the window's top-left corner at `at` (main window points) or else at the next place of a cascade; dragging a document tab more than 24 pt above or below the tab strip does this at the pointer; the window holding the current document takes the tools, a click in another document's window makes its document the current one; a document already floating stays where it is; returns {floating} (document indices)",
|
||||
),
|
||||
(
|
||||
"window.arrange.floatAllInWindows",
|
||||
@@ -609,7 +609,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"window.arrange.consolidateAllWindows",
|
||||
"Consolidate All Windows",
|
||||
"",
|
||||
"{} show one document at a time again, under its tab, after Tile, Float in Window, Float All in Windows or Cascade; returns {tiled: false, floating: []}",
|
||||
"{index?} show one document at a time again, under its tab, after Tile, Float in Window, Float All in Windows or Cascade; returns {tiled: false, floating: []}; with `index`, only document `index` goes back under its tab and becomes the current one, the other windows and Tile staying as they are, as when a document window is dragged by its title bar (an OS window by the tab at its top) onto the tab strip; returns {tiled, floating} (document indices)",
|
||||
),
|
||||
(
|
||||
"window.nextDocument",
|
||||
|
||||
Reference in New Issue
Block a user