fix: restore comfortable tab height with native control alignment
Verify Tessera GUI / verify (push) Failing after 1m39s

This commit is contained in:
2026-10-25 15:01:32 +08:00
parent 846c99a991
commit 7b769b1477
8 changed files with 125 additions and 14 deletions
Generated
+3
View File
@@ -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",
+5
View File
@@ -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
+36 -5
View File
@@ -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.
+6
View File
@@ -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() {
+2
View File
@@ -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;
+56
View File
@@ -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));
}
}
}
}
+7 -4
View File
@@ -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
View File
@@ -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 {