fix: restore comfortable tab height with native control alignment
Verify Tessera GUI / verify (push) Failing after 1m39s
Verify Tessera GUI / verify (push) Failing after 1m39s
This commit is contained in:
Generated
+3
@@ -1652,6 +1652,9 @@ dependencies = [
|
||||
"core",
|
||||
"cosmic-text",
|
||||
"frontend",
|
||||
"objc2 0.5.2",
|
||||
"objc2-app-kit",
|
||||
"objc2-foundation 0.2.2",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"softbuffer",
|
||||
|
||||
@@ -19,6 +19,11 @@ tiny_http = "0.12"
|
||||
unicode-segmentation = "1.12"
|
||||
winit = "0.30.13"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc2 = "0.5.2"
|
||||
objc2-app-kit = { version = "0.2.2", default-features = false, features = ["NSButton", "NSView", "NSWindow"] }
|
||||
objc2-foundation = { version = "0.2.2", default-features = false, features = ["NSGeometry"] }
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
|
||||
@@ -4,9 +4,9 @@ This audit follows the full conversation, including later corrections. Implement
|
||||
|
||||
| Requirement | Implementation | Evidence / limits |
|
||||
| --- | --- | --- |
|
||||
| One top row with native macOS controls | Transparent/full-size native title bar; reserved control space; 28 px tab strip centered with AppKit traffic lights | Actual Retina macOS windows viewed; native close/minimize/fullscreen controls retained |
|
||||
| One top row with native macOS controls | Transparent/full-size native title bar; reserved control space; 40 px tab strip; native AppKit controls repositioned to its y=20 center | Actual Retina macOS windows viewed; native close/minimize/fullscreen controls retained |
|
||||
| Continuous left/top chrome | One neutral background; 56 px left rail; inset content panel | Actual main and detached windows viewed; no Tools label or rail launchers |
|
||||
| Compact symmetric round tabs, softer active state | 88–208 px width; 24 px pill with equal 2 px vertical insets; 13 px labels and matching 18 px icon/close slots; no hard active stroke | Native settled UI viewed; hover and selected use the same bounds |
|
||||
| Compact symmetric round tabs, softer active state | 88–208 px width; 32 px pill with equal 4 px vertical insets; 13 px labels and matching 18 px icon/close slots; no hard active stroke | Native settled UI viewed; hover and selected use the same bounds |
|
||||
| + and startup default page | One startup request guarded against duplicate resume; + release-inside; Cmd/Ctrl+T | Immediately after restart: pages().count = 1. Two native Cmd+T actions produced three tabs |
|
||||
| New-tab motion, filled and unfilled | 220 ms cubic ease; insertion grows from the previous trailing edge; full-strip geometry and + interpolate; removed tabs shrink without remaining interactive | Real native new/close/fill operations completed; settled compression/overflow viewed. No frame-by-frame recording or Chrome timing comparison |
|
||||
| Overflow and scrolling | Separate + reservation; no arrow buttons; logical-pixel wheel/trackpad scrolling and clipped offscreen tabs; invisible drag edge dwell retained | Native overflow reached; + in overflow increased VM count 7 → 8. Dragging out of + kept count at 7 |
|
||||
@@ -130,9 +130,9 @@ scrolling follows input directly, while insertion, closure and automatic reveal
|
||||
use the 220 ms transition. OS momentum remains ordinary subsequent wheel input.
|
||||
No touchscreen hardware verification is claimed.
|
||||
|
||||
The 28 px row centers tabs at y=14, matching the retained AppKit controls in the
|
||||
native macOS window. Pills are 24 px high, with 2 px top/bottom margins and 13 px
|
||||
labels. A 1 × 12 px divider separates adjacent inactive tabs; it is hidden beside
|
||||
The initial 28 px row was too shallow. The revised 40 px row centers tabs at
|
||||
y=20 and repositions the retained native AppKit controls onto the same center
|
||||
line. Pills are 32 px high, with 4 px top/bottom margins and 13 px labels. A 1 × 12 px divider separates adjacent inactive tabs; it is hidden beside
|
||||
an active or hovered tab. Hover and selection retain the same pill geometry.
|
||||
|
||||
Manual native checks covered startup, + and repeated Cmd+T, dense overflow,
|
||||
@@ -146,3 +146,34 @@ Formatting, all-target offline Clippy with warnings denied, release app bundling
|
||||
and whitespace checks passed. Existing geometry test fixtures were updated for
|
||||
pixel offsets and the denser layout; they were type-checked by Clippy but no
|
||||
cargo test, automated smoke script or Docker task was run.
|
||||
|
||||
|
||||
### Comfortable row height with native alignment — 2026-10-25
|
||||
|
||||
The current Chromium [layout constants](https://github.com/chromium/chromium/blob/main/chrome/browser/ui/layout_constants.cc)
|
||||
define a 35 px tab (34 + 1 px toolbar overlap) and 6 px strip padding, totaling
|
||||
41 px. Tessera uses a 40 px row with a symmetric 32 px capsule, retaining its
|
||||
88–208 px width range, separators, gesture scrolling and insertion motion.
|
||||
Only the in-app web view is available for agent-owned browser access, so this
|
||||
is a source/reference-image comparison, not a live measurement of Chrome.
|
||||
|
||||
A macOS-only adapter obtains the winit-owned content NSView and positions the
|
||||
three real standard window buttons at the row's center. It preserves their
|
||||
native sizes, horizontal positions and actions, and reapplies alignment before
|
||||
drawing after native layout changes. Fullscreen floating controls stay under
|
||||
AppKit management. Native bindings were already cached transitively; direct
|
||||
macOS-only dependencies now declare this use explicitly.
|
||||
|
||||
Manual resizing followed by repeated tab creation exposed a renderer panic:
|
||||
rounding at large absolute x positions could invert a rounded rectangle's
|
||||
clamp bounds while a tab grew from almost zero width. Rounded hit coverage now
|
||||
clamps in local coordinates with a radius bounded by half the rectangle size.
|
||||
|
||||
Final native checks passed for 18 consecutive Cmd+T insertions, dense overflow,
|
||||
wheel scrolling, five consecutive Cmd+W closures and native window zoom. The
|
||||
final process stayed running without stderr errors. Initial native screenshots
|
||||
showed the traffic lights, tab capsules and + centered at the same y position.
|
||||
Subsequent computer-control captures can display macOS's capture badge over the
|
||||
traffic lights. Fullscreen floating controls and mixed-DPI moves were not tested.
|
||||
Offline all-target Clippy with warnings denied, formatting, release app bundling
|
||||
and git whitespace checks passed; no cargo test or automated smoke script ran.
|
||||
|
||||
@@ -1071,6 +1071,8 @@ impl TesseraApp {
|
||||
.with_fullsize_content_view(true)
|
||||
};
|
||||
let window = Arc::new(event_loop.create_window(attributes)?);
|
||||
#[cfg(target_os = "macos")]
|
||||
crate::native_chrome::align_controls(&window);
|
||||
let context = Context::new(window.clone()).map_err(|error| anyhow::anyhow!("{error}"))?;
|
||||
let surface =
|
||||
Surface::new(&context, window.clone()).map_err(|error| anyhow::anyhow!("{error}"))?;
|
||||
@@ -1111,6 +1113,10 @@ impl TesseraApp {
|
||||
}
|
||||
|
||||
fn redraw(&mut self) {
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Some(window) = self.window.as_ref() {
|
||||
crate::native_chrome::align_controls(window);
|
||||
}
|
||||
match self.try_redraw() {
|
||||
Ok(()) => {
|
||||
if self.window.is_some() && self.surface.is_some() {
|
||||
|
||||
@@ -15,6 +15,8 @@ mod geometry;
|
||||
mod io_tasks;
|
||||
mod layout;
|
||||
mod mount;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod native_chrome;
|
||||
mod native_visual;
|
||||
mod object_list;
|
||||
mod presentation;
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
//! Keep AppKit's real window buttons aligned with the custom tab strip.
|
||||
use objc2_app_kit::{NSView, NSWindowButton, NSWindowStyleMask};
|
||||
use objc2_foundation::NSPoint;
|
||||
use winit::{
|
||||
raw_window_handle::{HasWindowHandle, RawWindowHandle},
|
||||
window::Window,
|
||||
};
|
||||
|
||||
/// Called only on winit's main thread, after native creation and before drawing.
|
||||
/// AppKit can restore its default button positions when laying out the title bar.
|
||||
pub fn align_controls(window: &Window) {
|
||||
let Ok(handle) = window.window_handle() else {
|
||||
return;
|
||||
};
|
||||
let RawWindowHandle::AppKit(handle) = handle.as_raw() else {
|
||||
return;
|
||||
};
|
||||
// SAFETY: winit owns this NSView for the lifetime of the borrowed Window.
|
||||
// All AppKit access is on the event-loop thread; no native pointers escape.
|
||||
unsafe {
|
||||
let view = &*handle.ns_view.as_ptr().cast::<NSView>();
|
||||
let Some(native) = view.window() else {
|
||||
return;
|
||||
};
|
||||
// Fullscreen traffic lights belong to AppKit's separate floating toolbar.
|
||||
if native.styleMask().contains(NSWindowStyleMask::FullScreen) {
|
||||
return;
|
||||
}
|
||||
let bounds = view.bounds();
|
||||
let center = f64::from(crate::tabs::HEIGHT) / 2.0;
|
||||
let content_y = if view.isFlipped() {
|
||||
bounds.origin.y + center
|
||||
} else {
|
||||
bounds.origin.y + bounds.size.height - center
|
||||
};
|
||||
for kind in [
|
||||
NSWindowButton::NSWindowCloseButton,
|
||||
NSWindowButton::NSWindowMiniaturizeButton,
|
||||
NSWindowButton::NSWindowZoomButton,
|
||||
] {
|
||||
let Some(button) = native.standardWindowButton(kind) else {
|
||||
continue;
|
||||
};
|
||||
let Some(parent) = button.superview() else {
|
||||
continue;
|
||||
};
|
||||
let target = parent.convertPoint_fromView(NSPoint::new(0.0, content_y), Some(view));
|
||||
let frame = button.frame();
|
||||
let y = target.y - frame.size.height / 2.0;
|
||||
// Preserve native button size, horizontal spacing and behavior.
|
||||
if (frame.origin.y - y).abs() > 0.01 {
|
||||
button.setFrameOrigin(NSPoint::new(frame.origin.x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -203,10 +203,13 @@ fn rounded_rect_contains(rect: Rect, radius: f32, x: f32, y: f32) -> bool {
|
||||
return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height;
|
||||
}
|
||||
|
||||
let closest_x = x.clamp(rect.x + radius, rect.x + rect.width - radius);
|
||||
let closest_y = y.clamp(rect.y + radius, rect.y + rect.height - radius);
|
||||
let dx = x - closest_x;
|
||||
let dy = y - closest_y;
|
||||
// Clamp in local coordinates: adding a large origin to a shrinking tab's
|
||||
// half-width can otherwise round the lower bound above the upper bound.
|
||||
let radius = radius.min(rect.width / 2.0).min(rect.height / 2.0);
|
||||
let local_x = x - rect.x;
|
||||
let local_y = y - rect.y;
|
||||
let dx = local_x - local_x.clamp(radius, rect.width - radius);
|
||||
let dy = local_y - local_y.clamp(radius, rect.height - radius);
|
||||
dx * dx + dy * dy <= radius * radius
|
||||
}
|
||||
|
||||
|
||||
+10
-5
@@ -9,8 +9,8 @@ use serde::{Deserialize, Serialize};
|
||||
mod motion;
|
||||
pub use motion::{Motion, Visual};
|
||||
|
||||
// AppKit traffic lights are centered 14 logical pixels below the top edge.
|
||||
pub const HEIGHT: f32 = 28.0;
|
||||
// Native traffic lights and all tab controls share this row's center line.
|
||||
pub const HEIGHT: f32 = 40.0;
|
||||
pub const INSET: f32 = 8.0;
|
||||
const NEW_TAB_SPACE: f32 = 44.0;
|
||||
const MIN_WIDTH: f32 = 88.0;
|
||||
@@ -129,7 +129,7 @@ fn items(start: Option<f32>, tabs: &[Tab], width: f32) -> Vec<Item<'_>> {
|
||||
.collect()
|
||||
}
|
||||
fn pill_rect(rect: Rect) -> Rect {
|
||||
Rect::new(rect.x + 2.0, 2.0, (rect.width - 4.0).max(0.0), HEIGHT - 4.0)
|
||||
Rect::new(rect.x + 2.0, 4.0, (rect.width - 4.0).max(0.0), HEIGHT - 8.0)
|
||||
}
|
||||
fn close_rect(rect: Rect) -> Rect {
|
||||
Rect::new(
|
||||
@@ -236,13 +236,18 @@ pub fn draw_insertion(
|
||||
|
||||
pub fn plus_rect(start: Option<f32>, tabs: &[Tab], width: f32) -> Rect {
|
||||
if tabs.len() as f32 * tab_width(tabs, width) > viewport(width).width {
|
||||
return Rect::new(width - 34.0, 2.0, 24.0, HEIGHT - 4.0);
|
||||
return Rect::new(width - 34.0, (HEIGHT - 28.0) / 2.0, 24.0, 28.0);
|
||||
}
|
||||
let end = items(start, tabs, width)
|
||||
.iter()
|
||||
.map(|item| item.rect.x + item.rect.width)
|
||||
.fold(LEADING, f32::max);
|
||||
Rect::new((end + 4.0).min(width - 34.0), 2.0, 24.0, HEIGHT - 4.0)
|
||||
Rect::new(
|
||||
(end + 4.0).min(width - 34.0),
|
||||
(HEIGHT - 28.0) / 2.0,
|
||||
24.0,
|
||||
28.0,
|
||||
)
|
||||
}
|
||||
|
||||
fn tab_style() -> TextStyle {
|
||||
|
||||
Reference in New Issue
Block a user