mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 01:14:29 -06:00
Window > New Window was grayed out. It now opens another view of the
current document, with its own zoom, center, rotation, artboard and
Outline or Preview, as a tab of its own. While a document has more than
one view, each tab shows the view's number after the name
("Untitled-1:2"). An edit in one view shows in every view of the
document.
Tile, Cascade, Float in Window, Float All in Windows and Consolidate All
Windows arrange each view as they arrange a document, and Tile and
Cascade are enabled with two views of one document. A new view of a view
in a floating window opens in a floating window too. File > Close and a
tab's close button close the view; the document's last view closes the
document, asking to save it first. Next Document and Previous Document
step through the views in tab order.
Each Tile pane and floating window draws its view in its own Outline or
Preview, and switching views with the Artboard tool targets the artboard
the view shows. A switch to another view of the current document resets
the tool, as a switch to another document does (Session::reset_tool).
Artwork held over another view's tab during a drag switches to that
view, and a drop there moves it.
Fixes #4116
322 lines
14 KiB
Rust
322 lines
14 KiB
Rust
use super::{
|
|
Arc, Clipboard, Color, CommandInfo, DocState, Document, DrawMode, EngineError, GradientPaint, Journal, NodeKind, PaintDefaults, Prefs, Result,
|
|
Session, ViewInfo, cmd, command_specs, file_access,
|
|
};
|
|
|
|
impl Session {
|
|
/// A session with no document open, the default preferences and the Selection tool. It also
|
|
/// installs the hooks the document and effects crates call back into: the loader and file reader
|
|
/// of placed documents, where an image's effects draw its fills and strokes, for clicks, and how
|
|
/// Vector Halftone's dots are sampled, for exports.
|
|
pub fn new() -> Self {
|
|
// Placed documents are read by the native format's loader.
|
|
vectorcraft_doc::placed_document::set_loader(cmd::place::document::read_document);
|
|
vectorcraft_doc::placed_document::set_file_reader(cmd::place::document::read_file_again);
|
|
vectorcraft_doc::placed_document::set_read_check(|path| file_access::check_read(path).is_ok());
|
|
// The background workers of placed documents read files in the scope that asked.
|
|
vectorcraft_render::placed_document::set_scope_capture(|| {
|
|
let scope = file_access::ScopeHandle::capture();
|
|
Box::new(move |f: &mut dyn FnMut()| scope.run(f))
|
|
});
|
|
// Clicks find an image's own fills and strokes where their effects draw them.
|
|
vectorcraft_doc::hit::set_image_paint(vectorcraft_render::effects::image_paint);
|
|
// Clicks find a blend's steps and an envelope's distorted art where the canvas draws them,
|
|
// evaluated again when the fonts or the plug-ins change.
|
|
vectorcraft_doc::hit::set_live_art(vectorcraft_render::effects::expand_live, || {
|
|
vectorcraft_text::FontDb::global().generation().rotate_left(32) ^ vectorcraft_render::effects::plugin_revision()
|
|
});
|
|
// Exports bake Vector Halftone into the dots the renderer samples.
|
|
vectorcraft_render::effects::addons::halftone::set_art(vectorcraft_render::addons::halftone::art);
|
|
// OpenType SVG glyphs are drawn as the SVG importer reads their documents.
|
|
vectorcraft_text::set_svg_glyph_reader(vectorcraft_svg::glyph_art);
|
|
Self {
|
|
docs: vec![],
|
|
active: None,
|
|
prefs: Prefs::default(),
|
|
caps_lock: false,
|
|
paint: PaintDefaults::default(),
|
|
fill_active: true,
|
|
clipboard: Clipboard::default(),
|
|
journal: Journal::default(),
|
|
tool: vectorcraft_tools::create("selection"),
|
|
lender: None,
|
|
last_selection_tool: None,
|
|
last_view: ViewInfo::default(),
|
|
preview_memo: Default::default(),
|
|
depth: 0,
|
|
tool_panic: None,
|
|
tool_resets: 0,
|
|
tool_set_since_reset: false,
|
|
draw_mode: DrawMode::Normal,
|
|
draw_inside: None,
|
|
untitled_counter: 0,
|
|
menu: Default::default(),
|
|
gradient_stop: None,
|
|
active_appearance_item: None,
|
|
eyedropper_sample: None,
|
|
last_solid: Color::WHITE,
|
|
last_gradient: GradientPaint::new(Default::default()),
|
|
recent_colors: vec![],
|
|
pending_paint: None,
|
|
swatch_libraries: Default::default(),
|
|
freeform_point: None,
|
|
style_libraries: Default::default(),
|
|
symbol_libraries: Default::default(),
|
|
brush_libraries: Default::default(),
|
|
libraries: Default::default(),
|
|
pdf_preset_dir: None,
|
|
profile_dir: None,
|
|
plugin_dir: None,
|
|
plugin_output: None,
|
|
recent_urls: vec![],
|
|
trace_preset_changes: Default::default(),
|
|
ui_language: None,
|
|
journal_note: Default::default(),
|
|
note_depth: 1,
|
|
journal_query: false,
|
|
explicit_undo_group: None,
|
|
batch_stash: None,
|
|
batch_steps: None,
|
|
place_hand_off: None,
|
|
queue_hand_off: None,
|
|
place_queue_roots: None,
|
|
image_hand_off: None,
|
|
trace_hand_off: None,
|
|
place_cursor_requests: false,
|
|
package_hand_off: None,
|
|
screens_hand_off: None,
|
|
recovery: Default::default(),
|
|
in_tool_actions: false,
|
|
previewing: false,
|
|
envelope_defaults: None,
|
|
liquify_stroke: None,
|
|
plane_widget_press: false,
|
|
pointer_held: false,
|
|
press_options: None,
|
|
press_point: None,
|
|
ruler_guide: None,
|
|
port_marks: Default::default(),
|
|
font_search: None,
|
|
link_search: None,
|
|
search_rules: None,
|
|
deferred_overlay_cache: None,
|
|
search_threads: None,
|
|
link_stamps: Default::default(),
|
|
link_scan: None,
|
|
global_edit_options: Default::default(),
|
|
}
|
|
}
|
|
|
|
pub fn documents(&self) -> &[DocState] {
|
|
&self.docs
|
|
}
|
|
|
|
/// The open document with [`DocState::uid`] `uid` (it may have closed since it was looked up).
|
|
pub fn document_mut(&mut self, uid: u64) -> Option<&mut DocState> {
|
|
self.docs.iter_mut().find(|d| d.uid == uid)
|
|
}
|
|
|
|
pub fn active_index(&self) -> Option<usize> {
|
|
self.active
|
|
}
|
|
|
|
pub fn active(&self) -> Option<&DocState> {
|
|
self.active.and_then(|i| self.docs.get(i))
|
|
}
|
|
|
|
pub fn active_mut(&mut self) -> Option<&mut DocState> {
|
|
self.active.and_then(|i| self.docs.get_mut(i))
|
|
}
|
|
|
|
pub fn doc(&self) -> Result<&DocState> {
|
|
self.active().ok_or(EngineError::NoDocument)
|
|
}
|
|
|
|
pub fn doc_mut(&mut self) -> Result<&mut DocState> {
|
|
self.active_mut().ok_or(EngineError::NoDocument)
|
|
}
|
|
|
|
/// Finish the tool's pending work in the current document and give it fresh state, so nothing
|
|
/// (e.g. uncommitted typing) leaks into the document we are switching to.
|
|
fn reset_tool_for_doc_switch(&mut self) {
|
|
if self.active.is_none() {
|
|
return;
|
|
}
|
|
let view = self.last_view;
|
|
// The switch goes on whatever the lent tool's last actions did, as for the deactivation.
|
|
let _ = self.give_back_tool(view);
|
|
let acts = self.with_tool_cx(view, |t, cx| t.deactivate(cx));
|
|
let _ = self.apply_actions(acts);
|
|
// A batch leaves its interaction open in the document it leaves, to keep or roll back with
|
|
// the rest of the batch; anything else in progress (a drag) is cancelled.
|
|
if self.batch_stash.is_none() {
|
|
let _ = self.cancel_interaction();
|
|
}
|
|
self.keep_tool_settings();
|
|
self.tool = self.make_tool(self.tool.id());
|
|
self.tool_resets = self.tool_resets.wrapping_add(1);
|
|
self.tool_set_since_reset = false;
|
|
}
|
|
|
|
/// Finish the tool's pending work and give it fresh state, as a switch to another document
|
|
/// does, while the current document stays the same (a switch to another view of it).
|
|
pub fn reset_tool(&mut self) {
|
|
self.reset_tool_for_doc_switch();
|
|
}
|
|
|
|
pub fn set_active(&mut self, index: usize) -> bool {
|
|
if index < self.docs.len() {
|
|
if self.active != Some(index) {
|
|
self.reset_tool_for_doc_switch();
|
|
}
|
|
self.active = Some(index);
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
|
|
/// The name [`Session::next_untitled`] gives next (New Document's Name field).
|
|
pub fn peek_untitled(&self) -> String {
|
|
format!("Untitled-{}", self.untitled_counter + 1)
|
|
}
|
|
|
|
pub fn next_untitled(&mut self) -> String {
|
|
self.untitled_counter += 1;
|
|
format!("Untitled-{}", self.untitled_counter)
|
|
}
|
|
|
|
/// Text layout bounds are a cache (not saved): compute them for a document just read, or
|
|
/// selection boxes and hit testing would use the rough estimate until each text is edited, and
|
|
/// what is fitted to type's bounds (an envelope warping it) would come out differently. That
|
|
/// holds for the type in symbol definitions, pattern tiles, graph designs and opacity masks too,
|
|
/// not only in the layers (#1738).
|
|
pub(crate) fn refresh_text_bounds(doc: &mut Document) {
|
|
// Inline graphics in text take their size from their symbols' art (not saved either).
|
|
doc.resolve_inline_art();
|
|
let trees = doc.layers.iter_mut().chain(doc.symbols.iter_mut().map(|s| &mut s.art));
|
|
let trees = trees.chain(doc.graph_designs.iter_mut().map(|g| &mut g.art));
|
|
for tree in trees.chain(doc.patterns.iter_mut().flat_map(|p| p.art.iter_mut())) {
|
|
refresh_tree_text_bounds(tree);
|
|
}
|
|
}
|
|
|
|
/// Add a document and make it active. Between `edit.undoGroup.begin` and `edit.undoGroup.end`,
|
|
/// an undo group with the begun group's name opens in it: here outside a command, and after the
|
|
/// command that adds it otherwise ([`Session::execute`]).
|
|
pub fn add_document(&mut self, mut doc: Document, path: Option<String>) -> usize {
|
|
// Every edit is checked against the document before it, so the first one is within the cap
|
|
// (importers and the native reader have seen to theirs already, with a warning).
|
|
doc.limit_depth();
|
|
Self::refresh_text_bounds(&mut doc);
|
|
// Guides from files saved before guides had layers go on a layer.
|
|
doc.adopt_guides();
|
|
cmd::dimension::conform(&mut doc);
|
|
self.reset_tool_for_doc_switch();
|
|
let mut st = DocState::new(doc, path);
|
|
st.history.limit = self.prefs.history_states as usize;
|
|
st.opened_as_tab = self.prefs.open_documents_as_tabs;
|
|
self.docs.push(st);
|
|
let i = self.docs.len() - 1;
|
|
self.active = Some(i);
|
|
if self.depth == 0
|
|
&& let Some(label) = self.explicit_undo_group.clone()
|
|
{
|
|
self.begin_named_undo_group(label);
|
|
}
|
|
i
|
|
}
|
|
|
|
/// Replace the document in tab `index` (File → Revert): new content, cleared history and
|
|
/// selection, saved state. The tab keeps its place, path, format and view.
|
|
pub fn replace_document(&mut self, index: usize, mut doc: Document) -> bool {
|
|
if index >= self.docs.len() {
|
|
return false;
|
|
}
|
|
doc.limit_depth();
|
|
Self::refresh_text_bounds(&mut doc);
|
|
doc.adopt_guides();
|
|
cmd::dimension::conform(&mut doc);
|
|
if self.active == Some(index) {
|
|
// Pending tool work (typing, a drag) belongs to the content being thrown away.
|
|
self.reset_tool_for_doc_switch();
|
|
}
|
|
let old = &self.docs[index];
|
|
let mut st = DocState::new(doc, old.path.clone());
|
|
st.history.limit = old.history.limit;
|
|
// Same open document (caches keyed by uid stay valid); a new revision redraws it.
|
|
st.uid = old.uid;
|
|
st.opened_as_tab = old.opened_as_tab;
|
|
st.revision = old.revision + 1;
|
|
st.format = old.format;
|
|
st.save_options = old.save_options.clone();
|
|
// The remembered artboard pick follows the artboards it named when the file read back has
|
|
// the artboards the document had at the save, in the same order and with the same ids (a
|
|
// native file keeps them; a PDF's pages get new ids in page order); otherwise it is left
|
|
// out.
|
|
if old.save_artboards.is_empty() || st.doc.artboards.iter().map(|a| a.id).eq(old.save_artboards.iter().copied()) {
|
|
st.save_artboards = old.save_artboards.clone();
|
|
} else {
|
|
for k in cmd::fileio::ARTBOARD_PARAMS {
|
|
st.save_options.remove(k);
|
|
}
|
|
}
|
|
st.converted = old.converted;
|
|
st.imported_from = old.imported_from.clone();
|
|
st.import_losses = old.import_losses.clone();
|
|
st.view = old.view.clone();
|
|
st.layers_open = old.layers_open.clone();
|
|
let old = std::mem::replace(&mut self.docs[index], st);
|
|
if let Some(stash) = &mut self.batch_stash {
|
|
stash.push(old);
|
|
}
|
|
true
|
|
}
|
|
|
|
pub fn close_document(&mut self, index: usize) -> bool {
|
|
if index >= self.docs.len() {
|
|
return false;
|
|
}
|
|
self.reset_tool_for_doc_switch();
|
|
// Closed (saved or discarded): nothing left to recover.
|
|
let uid = self.docs[index].uid;
|
|
cmd::recovery::forget(self, uid);
|
|
let old = self.docs.remove(index);
|
|
if let Some(stash) = &mut self.batch_stash {
|
|
stash.push(old);
|
|
}
|
|
self.active = if self.docs.is_empty() { None } else { Some(index.min(self.docs.len() - 1)) };
|
|
true
|
|
}
|
|
|
|
/// Commands with enablement (for menus, palette, MCP `list_commands`).
|
|
pub fn commands(&self) -> Vec<CommandInfo> {
|
|
command_specs().iter().map(|c| c.info(self)).collect()
|
|
}
|
|
}
|
|
|
|
/// Lay out the type in tree `n` (opacity mask art included) whose bounds aren't cached. Only the
|
|
/// nodes on the way to such type are copied.
|
|
fn refresh_tree_text_bounds(n: &mut Arc<vectorcraft_doc::Node>) {
|
|
fn stale(n: &vectorcraft_doc::Node) -> bool {
|
|
matches!(&n.kind, NodeKind::Text(t) if t.cached_bounds.is_none())
|
|
|| n.extras().mask.as_deref().is_some_and(|m| stale(&m.art))
|
|
|| n.children().is_some_and(|ch| ch.iter().any(|c| stale(c)))
|
|
}
|
|
if !stale(n) {
|
|
return;
|
|
}
|
|
let n = Arc::make_mut(n);
|
|
if let NodeKind::Text(t) = &mut n.kind
|
|
&& t.cached_bounds.is_none()
|
|
{
|
|
cmd::typecmd::refresh_bounds(t);
|
|
}
|
|
if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_deref_mut()) {
|
|
refresh_tree_text_bounds(&mut m.art);
|
|
}
|
|
for c in n.children_mut().into_iter().flatten() {
|
|
refresh_tree_text_bounds(c);
|
|
}
|
|
}
|