Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
12464b7
test(tabs): the tab strip stays on its pane in every frame of a key r…
lawrencecchen Nov 3, 2026
43910d3
test(tabs): the strip tests use a terminal-like content anchor and a …
lawrencecchen Nov 3, 2026
294737e
test(tabs): the key-press case names its anchor with a switch (it com…
lawrencecchen Nov 3, 2026
c884b2a
test(tabs): a held key keeps each strip on its pane through every ret…
lawrencecchen Nov 3, 2026
ff384d2
Merge remote-tracking branch 'origin/feat-cmux-next' into fix/frame-o…
lawrencecchen Nov 3, 2026
e94ae9e
debug(tabs): film fixture: a 1 device-px #FFFF00 outline on every tab…
lawrencecchen Nov 3, 2026
813ef9a
fix(tabs): in a keyboard resize the tab strip follows its pane in eve…
lawrencecchen Nov 3, 2026
d1d4dd4
test(panes): PaneContentAreaHoldTests' content is its own class (TabS…
lawrencecchen Nov 3, 2026
f3de79c
test(pane motion): the shell never shows inside a pane rect, at any t…
lawrencecchen Nov 3, 2026
07f254a
test(tabs): the key-resize strip bench counts what the frame's commit…
lawrencecchen Nov 3, 2026
22ff6b7
Merge remote-tracking branch 'origin/fix/frame-owner-s1-strip-tick' i…
lawrencecchen Nov 3, 2026
8579f4c
Merge remote-tracking branch 'origin/feat-cmux-next' into fix/frame-o…
lawrencecchen Nov 3, 2026
fdf76bc
fix(motion): FrameTick.motionTimestamp is a Double (CmuxNextDesignTok…
lawrencecchen Nov 3, 2026
2c161ed
Merge remote-tracking branch 'origin/fix/frame-owner-s1-strip-tick' i…
lawrencecchen Nov 3, 2026
9830311
Merge remote-tracking branch 'origin/feat-cmux-next' into fix/frame-o…
lawrencecchen Nov 3, 2026
0186371
Merge remote-tracking branch 'origin/feat-cmux-next' into fix/frame-o…
lawrencecchen Nov 3, 2026
5dd6172
test(panes): a chat's tab-row fit that flips during a key resize wait…
lawrencecchen Nov 3, 2026
9ac8d4f
fix(panes): a key resize's area hold keeps the tab-row decision it be…
lawrencecchen Nov 3, 2026
666aa5c
test(resize keys): S3a's takeover test reads both hold shapes and run…
lawrencecchen Nov 3, 2026
ffa0b62
fix(tabs): a strip move begins when it is made again, not at the fram…
lawrencecchen Nov 3, 2026
b75090d
panes: the tab-row latch's note sits on its property (CmuxNextApp lin…
lawrencecchen Nov 3, 2026
a87cf1f
fix(pane motion): containers follow the pane in every frame, only pro…
lawrencecchen Nov 3, 2026
b46792a
test(pane motion): the S3 ON cases import CmuxNextTerminal as testabl…
lawrencecchen Nov 3, 2026
5cead39
Merge remote-tracking branch 'origin/fix/frame-owner-s1-strip-tick' i…
lawrencecchen Nov 3, 2026
e79b747
fix(pane motion): S3's ON cases check each host against its pane's ar…
lawrencecchen Nov 3, 2026
6ea84b8
fix(pane motion): a terminal that joins during a key hold measures it…
lawrencecchen Nov 3, 2026
ff2295a
fix(pane motion): a closing pane's sides follow its neighbours' frame…
lawrencecchen Nov 3, 2026
75dbed3
fix(pane motion): review round 2 (Codex, Claude): a reversed press, a…
lawrencecchen Nov 3, 2026
f4d815e
fix(browser): a page hidden and shown again during its pane's key hol…
lawrencecchen Nov 3, 2026
7827a1f
fix(pane motion): a close taken back starts from where it shows, read…
lawrencecchen Nov 3, 2026
1aa7483
Merge remote-tracking branch 'origin/feat-cmux-next' into fix/frame-o…
lawrencecchen Nov 3, 2026
a6d4037
test(pane motion): two run() calls in PaneContentPolicyTests are mark…
lawrencecchen Nov 3, 2026
d67e03c
fix(pane motion): a closing pane with a free side rests at zero size …
lawrencecchen Nov 3, 2026
df840ec
Merge remote-tracking branch 'origin/feat-cmux-next' into fix/frame-o…
lawrencecchen Nov 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ extension PaneContentView {
/// `leaving`, retired right after, does not count as content above it.
func install(_ view: NSView, replacing leaving: NSView? = nil) {
guard view.superview === contentHost else {
view.frame = contentHost.contentBounds
view.frame = contentHost.contentBounds(for: view)
view.autoresizingMask = [.width, .height]
contentHost.addSubview(view)
refreshNativeToolOwner()
Expand All @@ -64,7 +64,7 @@ extension PaneContentView {
view.autoresizingMask = []
needsLayout = true
} else {
if view.frame != contentHost.contentBounds { view.frame = contentHost.contentBounds }
if view.frame != contentHost.contentBounds(for: view) { view.frame = contentHost.contentBounds(for: view) }
view.autoresizingMask = [.width, .height]
}
// Above what still shows (a backdrop or a held page); hidden parked
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -371,8 +371,11 @@ final class CEFHostView: NSView {
final class CEFTabContentView: NSView, PaneMotionSettling, PaneResizeGestureFollowing {
weak var tab: CEFTab?
private let snapshotView = NSImageView()
/// The page's size through a person's resize drag (`paneResizeGestureDidChange`); nil otherwise.
/// The page's size through a person's resize drag (`paneResizeGestureDidChange`) or a key resize's hold
/// (``holdPage(for:)``); nil otherwise.
private(set) var heldPageSize: CGSize?
/// The edges a key resize's hold keeps the page on (FRAME OWNER S3); nil: a drag's top-leading corner.
var pageMotionHold: PaneMotionHold?
/// The page windows told of a resize-sync drag (`BrowserDragResize.resizeSync`), to tell its end wherever the
/// page is by then; empty otherwise.
private(set) var syncedDragHostings: [CEFLayerHosting] = []
Expand Down Expand Up @@ -423,7 +426,7 @@ final class CEFTabContentView: NSView, PaneMotionSettling, PaneResizeGestureFoll
/// shared host view go, as when it leaves the window, at no window move.
override func viewDidHide() {
super.viewDidHide()
dropHold()
dropHold(keepingKeyHold: true)
refreshSyncedDrag()
tab?.contentDidDisappear()
}
Expand All @@ -435,6 +438,7 @@ final class CEFTabContentView: NSView, PaneMotionSettling, PaneResizeGestureFoll
refreshSyncedDrag()
tab.pageThemeDidChange()
tab.contentDidAppear(in: self)
resumePageHold()
}

override func layout() {
Expand Down Expand Up @@ -483,9 +487,9 @@ final class CEFTabContentView: NSView, PaneMotionSettling, PaneResizeGestureFoll
} else if subview is CEFDevToolsDivider {
frame = frames.grab
} else if fitPage == nil, let held = heldPageSize, subview === tab?.host.hostView {
// A resize drag: the page keeps its size at the page frame's top-left corner, and the
// pane clips it (`CEFClipShape` and the fork's visible rect read the held host).
frame = CGRect(x: frames.page.minX, y: frames.page.maxY - held.height, width: held.width, height: held.height)
// A resize drag: the page keeps its size at the page frame's top-left corner (a key hold: on its
// edges), and the pane clips it (`CEFClipShape` and the fork's visible rect read the held host).
frame = (pageMotionHold ?? PaneMotionHold(trailing: false, bottom: false)).producerFrame(held, in: frames.page)
} else {
frame = fitPage ?? frames.page
}
Expand Down Expand Up @@ -519,7 +523,8 @@ final class CEFTabContentView: NSView, PaneMotionSettling, PaneResizeGestureFoll
func paneResizeGestureDidChange(active: Bool) {
guard active else {
endSyncedDrag()
guard heldPageSize != nil else { return }
// A key resize's page hold (FRAME OWNER S3) is its pane's to end, not the drag's.
guard heldPageSize != nil, pageMotionHold == nil else { return }
dropHold()
layoutContent()
return
Expand All @@ -528,16 +533,21 @@ final class CEFTabContentView: NSView, PaneMotionSettling, PaneResizeGestureFoll
startSyncedDrag()
return
}
guard BrowserResizeTunables.dragResize.value == .hold, heldPageSize == nil, window != nil, pageCanHold,
let tab else { return }
let size = tab.host.hostView.frame.size
guard size.width > 0, size.height > 0 else { return }
heldPageSize = size
guard BrowserResizeTunables.dragResize.value == .hold else { return }
startPageHold()
}

/// The page keeps the size it has now; false when it cannot (not a plain page here, no size, out of a window).
@discardableResult
func startPageHold() -> Bool {
guard canStartPageHold, let tab else { return false }
heldPageSize = tab.host.hostView.frame.size
// The held page is cut at this view's edges: its layer here, and the page window by the
// fork's visible rect, which follows every ancestor whose layer masks to its bounds.
layer?.masksToBounds = true
// A see-through page (a cmux page) shows the pane under it, so its gap does too.
layer?.backgroundColor = tab.host.hostView.pageSeeThrough ? nil : tab.pageBackdropColor
return true
}

/// Every page window over this pane waits for its frame at each size of the drag: the pane's shared one from
Expand Down Expand Up @@ -574,17 +584,25 @@ final class CEFTabContentView: NSView, PaneMotionSettling, PaneResizeGestureFoll
for hosting in hostings { hosting.paneResizeDrag(active: false) }
}

/// A page hold can begin now: none yet, in a window, a plain page with a size.
var canStartPageHold: Bool {
guard heldPageSize == nil, window != nil, pageCanHold, let size = tab?.host.hostView.frame.size else { return false }
return size.width > 0 && size.height > 0
}

/// A plain page shown here: no docked DevTools taking room, side panel or device fit, which place the page
/// themselves. Views kept hidden after an unreported DevTools open take no room (#32253): the page still holds.
private var pageCanHold: Bool {
var pageCanHold: Bool {
guard let tab else { return false }
return tab.host.hostView.superview === self && !tab.devToolsController.reservesRoom
&& !(tab.devToolsController.deviceModeOn && tab.devToolsController.views != nil)
&& tab.sidePanel.state == nil && tab.deviceEmulation == nil
}

/// Lets go of a held page size; the next layout gives the page its frame.
private func dropHold() {
/// Lets go of a held page size; the next layout gives the page its frame. `keepingKeyHold`: a key resize's hold stays
/// asked for, and the page holds again when it shows (`resumePageHold`).
func dropHold(keepingKeyHold: Bool = false) {
if !keepingKeyHold { pageMotionHold = nil }
guard heldPageSize != nil else { return }
heldPageSize = nil
layer?.masksToBounds = false
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import AppKit
import CmuxNextDesign

/// A tab's page view that keeps its size through a keyboard resize's hold while the browser chrome around it follows its
/// pane (FRAME OWNER REFACTOR S3, ``BrowserChromeView`` as ``PaneMotionHoldFollowing``).
@MainActor
protocol BrowserPagePaneMotionHolding: NSView {
/// Whether the page holds through a key hold now, or could start one (a plain page in a window).
var holdsPageInPaneMotion: Bool { get }
/// The pane's hold began or changed its edges (the page keeps its size on them), or ended (nil: it lays out once).
func holdPage(for hold: PaneMotionHold?)
}

/// A Chromium page draws in another process, some frames after its window takes a size: through a key resize the page
/// keeps the size it had when the hold began, fixed at the pane edges that do not move, the pane clips it, and this
/// view paints the page's own backdrop beside it, never the shell (the drag hold's path, `startPageHold`). It lays out
/// once when the hold ends, before the pane hears that the motion landed. A page that cannot hold any longer (DevTools
/// docked, a side panel or a fit came up, the tab was hidden mid-hold) lays out live at the chrome's page area, as a page
/// does in a split or close spring: never the shell, the chrome's own page area is beside it. A page hidden and shown again
/// during the hold holds again (`resumePageHold`).
extension CEFTabContentView: BrowserPagePaneMotionHolding {
var holdsPageInPaneMotion: Bool { pageMotionHold != nil || canStartPageHold }

/// Shown again during its pane's key hold (a tab switch away and back): it holds its page again, at the size the page
/// has now; one that cannot lays out live, as it would at the hold's start.
func resumePageHold() {
guard pageMotionHold != nil, heldPageSize == nil else { return }
if startPageHold() { needsLayout = true } else { pageMotionHold = nil }
}

func holdPage(for hold: PaneMotionHold?) {
guard let hold else {
guard pageMotionHold != nil else { return }
dropHold()
return
}
// A drag's own hold, or a page that cannot hold (DevTools docked, a side panel, a fit), lays out as before.
if pageMotionHold == nil, !startPageHold() { return }
pageMotionHold = hold
needsLayout = true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ extension BrowserChromeView {
/// back into the window (R55). The pane re-pins its header band itself.
public override func viewDidUnhide() {
super.viewDidUnhide()
// Shown during its pane's key hold (a tab switch): it follows the pane from this frame (FRAME OWNER S3).
(superview as? PaneContentAreaView)?.followerDidShow()
guard window != nil else { return }
updateColors()
renderPrompt()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
public import AppKit
public import CmuxNextDesign

/// FRAME OWNER REFACTOR S3 (`motion.frameOwner.contentPolicy`): in a keyboard resize the browser chrome is a container
/// that follows its pane in every frame: its toolbar, progress line and page area lay out at the pane's size, and the
/// page area's own backdrop fills what the page does not. Only the page keeps its size (``BrowserPagePaneMotionHolding``:
/// a Chromium page). A page that cannot hold (a WebKit page, docked DevTools, a side panel, a device fit) keeps the
/// whole chrome held by its pane's content area, as before S3.
extension BrowserChromeView: PaneMotionHoldFollowing {
public var followsPaneMotion: Bool { (tab.contentView as? any BrowserPagePaneMotionHolding)?.holdsPageInPaneMotion ?? false }

public func followPaneMotion(_ hold: PaneMotionHold?, heldSize: CGSize?) {
paneMotionHold = hold
(tab.contentView as? any BrowserPagePaneMotionHolding)?.holdPage(for: hold)
needsLayout = true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ public final class BrowserChromeView: NSView {
var httpsFallback = HTTPSUpgradeFallback()
private var showsStop = false
var isToolbarHidden = false
/// The key-resize hold this chrome follows its pane through (FRAME OWNER S3, `BrowserChromeView+PaneMotion`); nil at rest.
var paneMotionHold: PaneMotionHold?
/// The edge the toolbar rows sit on (`browser.toolbarPosition`); the pane sets it.
public var toolbarPosition: ToolbarPosition = .top {
didSet { if toolbarPosition != oldValue { applyToolbarPosition() } }
Expand Down Expand Up @@ -369,6 +371,8 @@ public final class BrowserChromeView: NSView {
content.trailingAnchor.constraint(equalTo: contentContainer.trailingAnchor),
content.bottomAnchor.constraint(equalTo: contentContainer.bottomAnchor),
])
// A page that comes in while the chrome follows its pane through a key hold keeps the pane's page size too.
if let paneMotionHold { (content as? any BrowserPagePaneMotionHolding)?.holdPage(for: paneMotionHold) }
pageBars.findBarIfMade?.tab = tab
addressBar.allowsChromiumSchemes = tab.engineKind == .cef
extensionToolbar.bind(tab)
Expand Down
Loading
Loading