mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-22 19:08:39 +00:00
#968: centre the macOS window buttons on the app bar
AppKit places close, minimize and zoom in a standard title bar, shorter than the 44-point app bar they sit over, so they rode high on it. They are now moved to the middle of the bar, inset from its left as far as from its top, with the title bar container made as tall as the bar so they stay clickable. AppKit lays the title bar out again on resize and when leaving full screen, so the placement runs every frame and sets a frame only when it is out of place. The app bar keeps the room the buttons take, measured, instead of a fixed 78 points. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
5ff1b97376
commit
258d61ced0
7 files changed
+100
-10
No files matched your search
Generated
+1
@@ -4335,6 +4335,7 @@ dependencies = [
|
||||
"objc2-core-foundation",
|
||||
"objc2-core-text",
|
||||
"objc2-foundation 0.3.2",
|
||||
"raw-window-handle",
|
||||
"rfd",
|
||||
"serde_json",
|
||||
"vectorcraft-engine",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adob
|
||||
|
||||
This file tracks **how far we are and what's left**. Time estimates are wall-clock hours of continuous Claude Opus 5.5 agent work (including builds and the CI gate), given both for **one agent** and for **4–6 parallel agents** on disjoint crates. They are counted from the remaining work (see [Parity estimate](#parity-estimate)), calibrated against measured throughput, and updated as work lands.
|
||||
|
||||
_Last updated: 2026-10-10 (after placed documents (File › Place of a `.vectorcraft` file, linked), M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS import running more of PostScript Level 3 (patterns, Type 3 fonts, mesh shadings, masked images, VM and resource probes), and a preview that stands in saying why (Discord feedback, #466, #469, #474), chords with modifiers recorded in Edit › Keyboard Shortcuts (#487), Type Object Selection by Path Only (#394), Anti-aliased Artwork (#394), Show Pixel Grid (#394), pen and touch taps pressed and lifted within one frame clicking where they tapped and the Linux/Wayland pen-tablet workaround (#491), bounding-box resizes landing on the other objects' anchors and centres with labels, and the Rotate, Scale, Shear and Reflect tools snapping their drags (#482), Shift-drag marquees that toggle the objects or anchors they reach and a Lasso whose Alt subtraction keeps the rest of the selection (#483), the mouse wheel stepping the focused numeric field, the dialogs' and preferences' drag values made such fields (#485), Show The Home Screen When No Documents Are Open (#394), Numbers Without Units Are Points (#394), Auto-Collapse Iconic Panels (#394), the Contextual Task Bar moved by its handle and its More Options menu pinning and resetting its position, a designed macOS DMG windoLine truncated
|
||||
_Last updated: 2026-10-10 (after placed documents (File › Place of a `.vectorcraft` file, linked), M4.14–M4.98, M8.1–M8.20, M14.4–M14.7, the MCP prompts, resource templates, completions and logging, the 2026-10-07 issue fixes, the Layers panel rework and the 2026-10-08 community contributions: Pen and shape-tool modifiers, path editing, Layers, Artboards, Swatches/Brushes and dialogs, Japanese composition and PDF text, Hebrew/Arabic type, interface languages (Spanish among them), the font menu, saved selections, MCP and EPS, the missing-font export warning (#319), open paths in the Shape Builder (#374), PDF/`.ai` import keeping strokes live and hidden and nested layers (#372), per-corner Live Corners (#381), SVG type opening at the size it draws at (#396), the Wayland clipboard with copied-file paste (#398), ruler guides that can be selected, moved and deleted (#414), two batches of Preferences options that were stored but unused (#394), skeleton reads with `document.find` for large documents over MCP (#416) and anchors dragged with Direct Selection snapping to points, segments and smart guides, clipped SVG layers opening as clipping layers (#420), PDF/`.ai` layers read on pages of tens of millions of operators (#419), Document Color Mode putting RGB greys on the black plate on request and converting pattern tiles (#421) and movable dialogs with Offset Path's Joins dropdown (#423) and numeric fields scrubbed by dragging their label (#400), stroke weight presets in the stroke unit (#426), the Appearance panel's Opacity popup (#430), type on a path brackets, flip and Type on a Path Options that keep the type in place (#429), Layers rows dragged with a ghost and dropped on the trash or New Layer (#435), tool flyouts opening on a right or corner press (#436), tool flyouts torn off into floating toolbars (#437), the Shape Builder's erase mode deleting edges too (#439), installed fonts registered outside the font folders and fonts installed while the app runs listed in the font menus (#443), alike circular corners on rectangles that aren't square (#442), tool cursors as OS cursors so the pointer no longer trails over the canvas (#444), artboards copied and pasted with their art (#446), guides dragged out of the rulers snapping to the art's side midpoints and the artboards, artboard guides (#451), the toolbar's double arrow and Window › Toolbars › Double Column switching between one and two columns in any window height (#455), the Smart Guides display options and Snapping Tolerance (#394), Font Size and Leading combo boxes with preset lists in the Character and Properties panels and the Control bar (#458), screenshots pasted from the clipboard on macOS, Windows and the web (#457), the Properties panel's Fill and Stroke matching the Control bar's, Illustrator's Window menu panel keys (Shift+F7 for Align…) and Alt-wheel zoom whatever the wheel preference (Discord feedback), the point/area type widget (Discord feedback), groups in legacy Illustrator .ai files (Discord feedback), a rotating desktop log file (#465), image controls in the Control bar and Properties panel: Image Trace with its presets, Mask, Crop Image, Embed/Unembed, and an Image Trace object's preset, Expand and Release (Discord feedback), the Pen working as the Anchor Point tool with Alt held over a selected path's handles and anchors, and anchor buttons that convert, connect and cut paths at the selected anchors (Discord feedback), Japanese hanging punctuation (burasagari, #463), Escape leaving Presentation Mode with no screen mode saved across restarts, documents dropped on the desktop app opening as tabs of their own and a note that an `.ai` file's art off its artboards isn't in the part read (#472), Add/Delete Anchor Point tool icons drawn from the Pen tool's (#479), EPS import running more of PostScript Level 3 (patterns, Type 3 fonts, mesh shadings, masked images, VM and resource probes), and a preview that stands in saying why (Discord feedback, #466, #469, #474), chords with modifiers recorded in Edit › Keyboard Shortcuts (#487), Type Object Selection by Path Only (#394), Anti-aliased Artwork (#394), Show Pixel Grid (#394), pen and touch taps pressed and lifted within one frame clicking where they tapped and the Linux/Wayland pen-tablet workaround (#491), bounding-box resizes landing on the other objects' anchors and centres with labels, and the Rotate, Scale, Shear and Reflect tools snapping their drags (#482), Shift-drag marquees that toggle the objects or anchors they reach and a Lasso whose Alt subtraction keeps the rest of the selection (#483), the mouse wheel stepping the focused numeric field, the dialogs' and preferences' drag values made such fields (#485), Show The Home Screen When No Documents Are Open (#394), Numbers Without Units Are Points (#394), Auto-Collapse Iconic Panels (#394), the Contextual Task Bar moved by its handle and its More Options menu pinning and resetting its position, a designed macOS DMG windoLine truncated
|
||||
|
||||
## Where we are
|
||||
|
||||
|
||||
@@ -51,10 +51,13 @@ windows = { version = "0.62", features = ["Win32_Graphics_DirectWrite"] }
|
||||
# with default features off). Pinned exactly: review the diff before bumping.
|
||||
muda = { version = "=0.21.1", default-features = false }
|
||||
objc2 = "0.6"
|
||||
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSMenu", "NSEvent", "NSPasteboard", "NSResponder", "NSTextInputContext"] }
|
||||
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSButton", "NSControl", "NSMenu", "NSEvent", "NSPasteboard", "NSResponder", "NSTextInputContext", "NSView", "NSWindow"] }
|
||||
# Files opened from Finder and the Dock (src/open_documents.rs) and pasted bitmaps (src/clipboard.rs); both already come with winit, muda and rfd.
|
||||
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "block2", "NSArray", "NSData", "NSNotification", "NSOperation", "NSString", "NSURL"] }
|
||||
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "block2", "NSArray", "NSData", "NSGeometry", "NSNotification", "NSOperation", "NSString", "NSURL"] }
|
||||
block2 = "0.6"
|
||||
# The window's AppKit view, to centre its buttons on the app bar (src/mac_window.rs). MIT OR
|
||||
# Apache-2.0 OR Zlib, already in the build through eframe and winit.
|
||||
raw-window-handle = "0.6"
|
||||
# CoreText's font manager (src/mac_fonts.rs): fonts that apps and font managers register from their
|
||||
# own folders (#579). Zlib OR Apache-2.0 OR MIT; objc2-core-foundation already comes with
|
||||
# objc2-foundation.
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
//! macOS: the window buttons (close, minimize, zoom) centred on the app bar (#968).
|
||||
//!
|
||||
//! AppKit puts the buttons in a standard title bar, shorter than the app bar they sit over, so they
|
||||
//! rode high on it. [`center_buttons`] moves them to the middle of the bar, inset from its left as
|
||||
//! far as from its top, and makes the title bar as tall as the app bar so they stay clickable.
|
||||
//! AppKit lays its title bar out again when the window resizes or leaves full screen, so this runs
|
||||
//! every frame and moves the buttons only when they are out of place.
|
||||
//!
|
||||
//! Reaching the title bar's views from the window's content view has no safe API, hence the scoped
|
||||
//! `unsafe_code` allowance.
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use objc2::runtime::AnyObject;
|
||||
use objc2_app_kit::{NSView, NSWindowButton, NSWindowStyleMask};
|
||||
use objc2_foundation::NSPoint;
|
||||
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
|
||||
|
||||
/// The room the app bar keeps at its left for the buttons in AppKit's own place, before they are
|
||||
/// centred (and in full screen, where AppKit shows them elsewhere): their width and a gap.
|
||||
pub const DEFAULT_INSET: f32 = 78.0;
|
||||
|
||||
/// Differences below this (in points) leave a frame as it is, so a frame in place isn't set again.
|
||||
const SLACK: f64 = 0.25;
|
||||
|
||||
/// Centre the window's buttons on an app bar `bar` egui points tall, at the window's `zoom` factor
|
||||
/// (native points per egui point). Returns the room the app bar keeps clear at its left for them,
|
||||
/// in egui points, or `None` when they aren't over the bar (full screen) or can't be found.
|
||||
pub fn center_buttons(frame: &eframe::Frame, zoom: f32, bar: f32) -> Option<f32> {
|
||||
let RawWindowHandle::AppKit(handle) = frame.window_handle().ok()?.as_raw() else {
|
||||
return None;
|
||||
};
|
||||
// SAFETY: the handle's view is the window's content view, which AppKit keeps alive while the
|
||||
// window is open, and eframe hands out the handle only while it is.
|
||||
let view = unsafe { handle.ns_view.cast::<NSView>().as_ref() };
|
||||
let window = view.window()?;
|
||||
if window.styleMask().contains(NSWindowStyleMask::FullScreen) {
|
||||
return None;
|
||||
}
|
||||
let close = window.standardWindowButton(NSWindowButton::CloseButton)?;
|
||||
let minimize = window.standardWindowButton(NSWindowButton::MiniaturizeButton)?;
|
||||
let zoom_button = window.standardWindowButton(NSWindowButton::ZoomButton)?;
|
||||
// The buttons sit in the title bar view, inside the title bar container that sets its height.
|
||||
// SAFETY: both views belong to the window, alive while it is; used right away.
|
||||
let container = unsafe { close.superview().and_then(|titlebar| titlebar.superview()) }?;
|
||||
// Another layout (a later macOS) is left as AppKit made it.
|
||||
if AnyObject::class(&container).name() != c"NSTitlebarContainerView" {
|
||||
return None;
|
||||
}
|
||||
let size = close.frame().size;
|
||||
let gap = minimize.frame().origin.x - (close.frame().origin.x + size.width);
|
||||
let height = f64::from(bar * zoom);
|
||||
let margin = (height - size.height) / 2.0;
|
||||
if !(margin.is_finite() && margin > 0.0 && gap.is_finite() && gap >= 0.0 && zoom > 0.0) {
|
||||
return None;
|
||||
}
|
||||
let mut bounds = container.frame();
|
||||
let top = window.frame().size.height - height;
|
||||
if (bounds.size.height - height).abs() > SLACK || (bounds.origin.y - top).abs() > SLACK {
|
||||
bounds.size.height = height;
|
||||
bounds.origin.y = top;
|
||||
container.setFrame(bounds);
|
||||
}
|
||||
let mut x = margin;
|
||||
for button in [close, minimize, zoom_button] {
|
||||
let at = NSPoint::new(x, (height - button.frame().size.height) / 2.0);
|
||||
let now = button.frame().origin;
|
||||
if (now.x - at.x).abs() > SLACK || (now.y - at.y).abs() > SLACK {
|
||||
button.setFrameOrigin(at);
|
||||
}
|
||||
x += size.width + gap;
|
||||
}
|
||||
// The last button's right edge (`x` steps one gap past it) and the margin again after it.
|
||||
Some(((x - gap + margin) / f64::from(zoom)) as f32)
|
||||
}
|
||||
@@ -31,6 +31,8 @@ mod mac_fonts;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac_menu;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac_window;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod open_documents;
|
||||
mod prefs_dir;
|
||||
mod printing;
|
||||
@@ -69,7 +71,14 @@ impl eframe::App for App {
|
||||
}
|
||||
file_access::unconfined(|| self.app.raw_input_hook(raw));
|
||||
}
|
||||
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||
#[cfg_attr(not(target_os = "macos"), allow(unused_variables))]
|
||||
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let bar = vectorcraft_ui_egui::chrome::APP_BAR_HEIGHT;
|
||||
let inset = mac_window::center_buttons(frame, ui.ctx().zoom_factor(), bar);
|
||||
self.app.titlebar_inset = inset.unwrap_or(mac_window::DEFAULT_INSET);
|
||||
}
|
||||
file_access::unconfined(|| self.app.ui(ui));
|
||||
#[cfg(target_os = "macos")]
|
||||
if self.app.take_ime_discard() {
|
||||
@@ -566,7 +575,6 @@ fn main() -> std::process::ExitCode {
|
||||
{
|
||||
app.graphics_adapter = Some("OpenGL (Windows 7 compatibility)".into());
|
||||
}
|
||||
app.integrated_titlebar = cfg!(target_os = "macos");
|
||||
app.custom_titlebar = CUSTOM_TITLEBAR;
|
||||
if let Some(port) = control_port {
|
||||
let rx = control_server::start(port, cc.egui_ctx.clone());
|
||||
|
||||
@@ -12,15 +12,18 @@ use crate::theme::{self, Tokens};
|
||||
use crate::widgets;
|
||||
use crate::{VectorcraftApp, icons, menus, titlebar};
|
||||
|
||||
/// The application bar's height, in points.
|
||||
pub const APP_BAR_HEIGHT: f32 = 44.0;
|
||||
|
||||
/// The application bar: brand mark, Home, menus, then Discord, search and the workspace switcher
|
||||
/// at the right. With [`VectorcraftApp::custom_titlebar`] it is also the window's title bar
|
||||
/// ([`titlebar`]): the caption buttons take the right end and the rest of the bar drags the window.
|
||||
pub fn app_bar(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let custom = app.custom_titlebar;
|
||||
let left = if app.integrated_titlebar { 78 } else { 8 };
|
||||
let left = app.titlebar_inset.clamp(8.0, f32::from(i8::MAX)) as i8;
|
||||
let frame = egui::Frame::NONE.fill(t.app_bar).inner_margin(egui::Margin { left, right: if custom { 0 } else { 14 }, top: 0, bottom: 0 });
|
||||
let bar = egui::Panel::top("app_bar").exact_size(44.0).frame(frame.stroke(Stroke::new(1.0, t.border))).show(ui, |ui| {
|
||||
let bar = egui::Panel::top("app_bar").exact_size(APP_BAR_HEIGHT).frame(frame.stroke(Stroke::new(1.0, t.border))).show(ui, |ui| {
|
||||
if custom {
|
||||
titlebar::drag_area(ui, ui.max_rect());
|
||||
}
|
||||
|
||||
@@ -314,8 +314,9 @@ pub struct VectorcraftApp {
|
||||
pub services: Services,
|
||||
pub canvas: CanvasCache,
|
||||
pub perf: Perf,
|
||||
/// macOS: draw our own title strip under the traffic lights.
|
||||
pub integrated_titlebar: bool,
|
||||
/// macOS: the room the app bar keeps at its left for the window buttons over it, in points
|
||||
/// (0 where the OS draws no buttons over the app bar).
|
||||
pub titlebar_inset: f32,
|
||||
/// Last applied effect (Effect → Apply Last Effect).
|
||||
pub last_effect: Option<(String, serde_json::Value)>,
|
||||
/// Commands run through [`Self::run`] so far: the native menu bar reads its rows again when it
|
||||
@@ -440,7 +441,7 @@ impl VectorcraftApp {
|
||||
cursors: Default::default(),
|
||||
},
|
||||
perf: Perf::default(),
|
||||
integrated_titlebar: false,
|
||||
titlebar_inset: 0.0,
|
||||
last_effect: None,
|
||||
run_count: 0,
|
||||
clipboard_out: None,
|
||||
|
||||
Reference in new issue
Block a user