From 9213ce806c4d023b1e9f27c9b5a2560b235e5ff5 Mon Sep 17 00:00:00 2001 From: John Jeong Date: Wed, 16 Sep 2026 20:57:31 +0900 Subject: [PATCH 1/3] Anchor the meeting panel at bottom center Expand toward available screen space and preserve the chosen edge on collapse across macOS and Windows/Linux. Add placement regression tests. ANLG-383. --- plugins/windows/src/window/floating_bar.rs | 128 +++++++++++++----- .../swift-lib/src/FloatingBarManager.swift | 53 ++------ .../swift-lib/src/FloatingBarPlacement.swift | 19 +++ .../tests/FloatingBarPlacementTests.swift | 42 ++++++ 4 files changed, 166 insertions(+), 76 deletions(-) create mode 100644 plugins/windows/swift-lib/src/FloatingBarPlacement.swift create mode 100644 plugins/windows/swift-lib/tests/FloatingBarPlacementTests.swift diff --git a/plugins/windows/src/window/floating_bar.rs b/plugins/windows/src/window/floating_bar.rs index 3a9b22f3475..e51ca5cdd75 100644 --- a/plugins/windows/src/window/floating_bar.rs +++ b/plugins/windows/src/window/floating_bar.rs @@ -100,29 +100,42 @@ pub(crate) mod layout { } } - pub fn top_right_origin( + pub fn bottom_center_origin( work_x: f64, work_y: f64, work_width: f64, - _work_height: f64, + work_height: f64, window_width: f64, - _window_height: f64, + window_height: f64, ) -> (f64, f64) { ( - work_x + work_width - window_width - SCREEN_MARGIN, - work_y + SCREEN_MARGIN, + work_x + (work_width - window_width) / 2.0, + work_y + work_height - window_height - SCREEN_MARGIN, ) } - pub fn resize_keep_top_right( + #[cfg(any(test, not(target_os = "macos")))] + pub fn expands_upward(y: f64, height: f64, work_y: f64, work_height: f64) -> bool { + y - work_y > work_y + work_height - y - height + } + + pub fn resize_anchored( x: f64, y: f64, current_width: f64, - _current_height: f64, + current_height: f64, next_width: f64, - _next_height: f64, + next_height: f64, + expands_upward: bool, ) -> (f64, f64) { - (x + current_width - next_width, y) + ( + x + (current_width - next_width) / 2.0, + if expands_upward { + y + current_height - next_height + } else { + y + }, + ) } pub fn clamp_to_work_area( @@ -257,12 +270,14 @@ mod platform { use tauri_specta::Event; use super::layout::{ - clamp_to_work_area, container_size, is_expanded, resize_keep_top_right, top_right_origin, + bottom_center_origin, clamp_to_work_area, container_size, expands_upward, is_expanded, + resize_anchored, }; use super::{FloatingBarState, WINDOW_LABEL}; use crate::Error; static APP_HANDLE: OnceLock> = OnceLock::new(); + static EXPANDS_UPWARD: Mutex = Mutex::new(true); static LAST_STATE: Mutex> = Mutex::new(None); pub fn set_app_handle(app: tauri::AppHandle) { @@ -382,27 +397,47 @@ mod platform { let size_changed = (current_size.width - width).abs() >= 0.5 || (current_size.height - height).abs() >= 0.5; - if size_changed { - window.set_size(Size::Logical(next_size))?; - } - - let (next_x, next_y) = - if force_default_position || current_position.x == 0.0 && current_position.y == 0.0 { - default_origin(window, width, height)? - } else if size_changed { - resize_keep_top_right( - current_position.x, + let (next_x, next_y) = if force_default_position { + if let Ok(mut direction) = EXPANDS_UPWARD.lock() { + *direction = true; + } + default_origin(window, width, height)? + } else if size_changed { + let mut direction = EXPANDS_UPWARD.lock().map_err(|_| { + Error::PanelError("floating bar placement lock poisoned".to_string()) + })?; + if height > current_size.height { + let monitor = window + .current_monitor()? + .or(window.app_handle().primary_monitor()?) + .ok_or(Error::MonitorNotFound)?; + let work = monitor.work_area(); + let origin = work.position.to_logical::(monitor.scale_factor()); + let size = work.size.to_logical::(monitor.scale_factor()); + *direction = expands_upward( current_position.y, - current_size.width, current_size.height, - width, - height, - ) - } else { - return Ok(()); - }; + origin.y, + size.height, + ); + } + resize_anchored( + current_position.x, + current_position.y, + current_size.width, + current_size.height, + width, + height, + *direction, + ) + } else { + return Ok(()); + }; let (clamped_x, clamped_y) = clamp_origin(window, next_x, next_y, width, height)?; + if size_changed { + window.set_size(Size::Logical(next_size))?; + } window.set_position(Position::Logical(LogicalPosition::new( clamped_x, clamped_y, )))?; @@ -424,7 +459,7 @@ mod platform { let work_area = monitor.work_area(); let origin = work_area.position.to_logical::(scale); let size = work_area.size.to_logical::(scale); - Ok(top_right_origin( + Ok(bottom_center_origin( origin.x, origin.y, size.width, @@ -502,18 +537,43 @@ mod tests { } #[test] - fn pins_the_default_origin_to_the_work_area_top_right() { + fn pins_the_default_origin_to_the_work_area_bottom_center() { + assert_eq!( + layout::bottom_center_origin(0.0, 0.0, 1920.0, 1080.0, 111.0, 67.0), + (904.5, 1005.0) + ); + } + + #[test] + fn keeps_the_top_center_anchor_when_expanding_downward() { assert_eq!( - layout::top_right_origin(0.0, 0.0, 1920.0, 1080.0, 111.0, 67.0), - (1801.0, 8.0) + layout::resize_anchored(1801.0, 8.0, 111.0, 67.0, 368.0, 459.0, false), + (1672.5, 8.0) ); } #[test] - fn keeps_the_top_right_anchor_when_resizing() { + fn keeps_the_bottom_center_anchor_through_expansion_and_collapse() { + let (x, y) = layout::resize_anchored(904.5, 1005.0, 111.0, 67.0, 368.0, 459.0, true); + assert_eq!((x, y), (776.0, 613.0)); + assert_eq!( + layout::resize_anchored(x, y, 368.0, 459.0, 111.0, 67.0, true), + (904.5, 1005.0) + ); + } + + #[test] + fn chooses_the_side_with_more_space_on_an_offset_monitor() { + assert!(layout::expands_upward(1005.0, 67.0, 40.0, 1040.0)); + assert!(!layout::expands_upward(48.0, 67.0, 40.0, 1040.0)); + assert!(!layout::expands_upward(0.0, 67.0, 0.0, 1080.0)); + } + + #[test] + fn centers_on_an_offset_monitor() { assert_eq!( - layout::resize_keep_top_right(1801.0, 8.0, 111.0, 67.0, 368.0, 459.0), - (1544.0, 8.0) + layout::bottom_center_origin(-1920.0, 40.0, 1920.0, 1040.0, 111.0, 67.0), + (-1015.5, 1005.0) ); } diff --git a/plugins/windows/swift-lib/src/FloatingBarManager.swift b/plugins/windows/swift-lib/src/FloatingBarManager.swift index 41cfaa287b9..3dedce96ef6 100644 --- a/plugins/windows/swift-lib/src/FloatingBarManager.swift +++ b/plugins/windows/swift-lib/src/FloatingBarManager.swift @@ -6,6 +6,7 @@ final class FloatingBarManager { static let shared = FloatingBarManager() private var panel: NSPanel? + private var expandsUpward = true private let model = FloatingBarViewModel() private let settingsModel = FloatingOverlaySettingsModel.shared private let placement = FloatingPanelPositionController() @@ -191,8 +192,8 @@ final class FloatingBarManager { followsPointer: false ) { screen, size in let frame = screen.visibleFrame - let x = frame.maxX - size.width - FloatingBarLayout.screenMargin - let y = frame.maxY - size.height - FloatingBarLayout.screenMargin + let x = frame.midX - size.width / 2 + let y = frame.minY + FloatingBarLayout.screenMargin return NSPoint(x: x, y: y) } } @@ -207,19 +208,13 @@ final class FloatingBarManager { panel.minSize = size guard previousSize != size else { return false } - let previousLayout = - layout(matching: previousSize) - ?? FloatingBarWindowLayout( - isExpanded: !nextLayout.isExpanded, - showsExpand: model.liveCaptionToggleVisible) - let previousAnchorOffset = controlAnchorOffset(for: previousLayout) + let workArea = (panel.screen ?? NSScreen.main)?.visibleFrame ?? panel.frame + if size.height > previousSize.height { + expandsUpward = FloatingBarPlacement.expandsUpward(frame: panel.frame, workArea: workArea) + } let nextAnchorOffset = controlAnchorOffset(for: nextLayout) - let anchor = placement.anchorPoint(for: panel, offset: previousAnchorOffset) - let frame = NSRect( - x: anchor.x - nextAnchorOffset.x, - y: anchor.y - nextAnchorOffset.y, - width: size.width, - height: size.height) + let frame = FloatingBarPlacement.resizedFrame( + panel.frame, size: size, workArea: workArea, expandsUpward: expandsUpward) placement.setFrame( panel, to: frame, @@ -252,35 +247,9 @@ final class FloatingBarManager { ) } - private func layout(matching size: NSSize) -> FloatingBarWindowLayout? { - let candidates = [ - FloatingBarWindowLayout(isExpanded: true, showsExpand: true), - FloatingBarWindowLayout(isExpanded: true, showsExpand: false), - FloatingBarWindowLayout(isExpanded: false, showsExpand: true), - FloatingBarWindowLayout(isExpanded: false, showsExpand: false), - ] - - return candidates.first { candidate in - let candidateSize = self.size(for: candidate) - return abs(candidateSize.width - size.width) < 0.5 - && abs(candidateSize.height - size.height) < 0.5 - } - } - private func controlAnchorOffset(for layout: FloatingBarWindowLayout) -> NSPoint { - if layout.isExpanded { - return NSPoint( - x: FloatingBarLayout.inset + FloatingBarLayout.expandedWidth - - FloatingBarLayout.compactHorizontalPadding, - y: FloatingBarLayout.inset + FloatingBarLayout.expandedHeight - ) - } - - return NSPoint( - x: FloatingBarLayout.inset + FloatingBarLayout.compactHorizontalPadding - + FloatingBarLayout.compactControlsWidth(showsExpand: layout.showsExpand), - y: FloatingBarLayout.inset + FloatingBarLayout.compactHeight - ) + let size = size(for: layout) + return NSPoint(x: size.width / 2, y: expandsUpward ? 0 : size.height) } private func startObservingDisplayChanges() { diff --git a/plugins/windows/swift-lib/src/FloatingBarPlacement.swift b/plugins/windows/swift-lib/src/FloatingBarPlacement.swift new file mode 100644 index 00000000000..37303932686 --- /dev/null +++ b/plugins/windows/swift-lib/src/FloatingBarPlacement.swift @@ -0,0 +1,19 @@ +import Cocoa + +enum FloatingBarPlacement { + static func expandsUpward(frame: NSRect, workArea: NSRect) -> Bool { + workArea.maxY - frame.maxY > frame.minY - workArea.minY + } + + static func resizedFrame( + _ frame: NSRect, size: NSSize, workArea: NSRect, expandsUpward: Bool + ) -> NSRect { + let x = frame.midX - size.width / 2 + let y = expandsUpward ? frame.minY : frame.maxY - size.height + return NSRect( + x: min(max(x, workArea.minX), max(workArea.minX, workArea.maxX - size.width)), + y: min(max(y, workArea.minY), max(workArea.minY, workArea.maxY - size.height)), + width: size.width, + height: size.height) + } +} diff --git a/plugins/windows/swift-lib/tests/FloatingBarPlacementTests.swift b/plugins/windows/swift-lib/tests/FloatingBarPlacementTests.swift new file mode 100644 index 00000000000..5230c9b7021 --- /dev/null +++ b/plugins/windows/swift-lib/tests/FloatingBarPlacementTests.swift @@ -0,0 +1,42 @@ +import Cocoa +import XCTest + +@testable import swift_lib + +final class FloatingBarPlacementTests: XCTestCase { + private let workArea = NSRect(x: -1920, y: 40, width: 1920, height: 1040) + private let compactSize = NSSize(width: 111, height: 67) + private let expandedSize = NSSize(width: 368, height: 459) + + func testBottomExpansionAndCollapsePreserveBottomCenter() { + let compact = NSRect(x: -1015.5, y: 48, width: 111, height: 67) + XCTAssertTrue(FloatingBarPlacement.expandsUpward(frame: compact, workArea: workArea)) + let expanded = FloatingBarPlacement.resizedFrame( + compact, size: expandedSize, workArea: workArea, expandsUpward: true) + XCTAssertEqual(expanded.midX, compact.midX) + XCTAssertEqual(expanded.minY, compact.minY) + XCTAssertEqual( + FloatingBarPlacement.resizedFrame( + expanded, size: compactSize, workArea: workArea, expandsUpward: true), compact) + } + + func testTopExpansionAndCollapsePreserveTopCenter() { + let compact = NSRect(x: -1015.5, y: 1005, width: 111, height: 67) + XCTAssertFalse(FloatingBarPlacement.expandsUpward(frame: compact, workArea: workArea)) + let expanded = FloatingBarPlacement.resizedFrame( + compact, size: expandedSize, workArea: workArea, expandsUpward: false) + XCTAssertEqual(expanded.midX, compact.midX) + XCTAssertEqual(expanded.maxY, compact.maxY) + XCTAssertEqual( + FloatingBarPlacement.resizedFrame( + expanded, size: compactSize, workArea: workArea, expandsUpward: false), compact) + } + + func testExpansionClampsAtDisplayEdge() { + let frame = NSRect(x: -112, y: 48, width: 111, height: 67) + let expanded = FloatingBarPlacement.resizedFrame( + frame, size: expandedSize, workArea: workArea, expandsUpward: true) + XCTAssertEqual(expanded.maxX, workArea.maxX) + XCTAssertTrue(workArea.contains(expanded)) + } +} From 8ea966278ca565c3dc548c811a21e4089551b853 Mon Sep 17 00:00:00 2001 From: John Jeong Date: Thu, 17 Sep 2026 09:03:07 +0900 Subject: [PATCH 2/3] Preserve panel placement after clamped expansion Reset new panel orientation and restore compact coordinates after edge clamping. Respect user movement and cover collapse regressions in Swift and Rust. ANLG-383. --- plugins/windows/src/window/floating_bar.rs | 62 ++++++++++++++++--- .../swift-lib/src/FloatingBarManager.swift | 12 +++- .../swift-lib/src/FloatingBarPlacement.swift | 7 ++- .../tests/FloatingBarPlacementTests.swift | 4 ++ 4 files changed, 76 insertions(+), 9 deletions(-) diff --git a/plugins/windows/src/window/floating_bar.rs b/plugins/windows/src/window/floating_bar.rs index e51ca5cdd75..05b89bd0564 100644 --- a/plugins/windows/src/window/floating_bar.rs +++ b/plugins/windows/src/window/floating_bar.rs @@ -119,6 +119,18 @@ pub(crate) mod layout { y - work_y > work_y + work_height - y - height } + pub fn collapse_anchor( + current: (f64, f64, f64, f64), + expansion: Option<((f64, f64, f64, f64), (f64, f64))>, + ) -> (f64, f64, f64, f64) { + if let Some((compact, expanded)) = expansion { + if (current.0 - expanded.0).abs() < 0.5 && (current.1 - expanded.1).abs() < 0.5 { + return compact; + } + } + current + } + pub fn resize_anchored( x: f64, y: f64, @@ -278,6 +290,7 @@ mod platform { static APP_HANDLE: OnceLock> = OnceLock::new(); static EXPANDS_UPWARD: Mutex = Mutex::new(true); + static EXPANSION: Mutex> = Mutex::new(None); static LAST_STATE: Mutex> = Mutex::new(None); pub fn set_app_handle(app: tauri::AppHandle) { @@ -397,6 +410,20 @@ mod platform { let size_changed = (current_size.width - width).abs() >= 0.5 || (current_size.height - height).abs() >= 0.5; + let mut expansion = EXPANSION + .lock() + .map_err(|_| Error::PanelError("floating bar placement lock poisoned".to_string()))?; + let mut previous = ( + current_position.x, + current_position.y, + current_size.width, + current_size.height, + ); + if force_default_position { + *expansion = None; + } else if height < current_size.height { + previous = super::layout::collapse_anchor(previous, expansion.take()); + } let (next_x, next_y) = if force_default_position { if let Ok(mut direction) = EXPANDS_UPWARD.lock() { *direction = true; @@ -422,19 +449,16 @@ mod platform { ); } resize_anchored( - current_position.x, - current_position.y, - current_size.width, - current_size.height, - width, - height, - *direction, + previous.0, previous.1, previous.2, previous.3, width, height, *direction, ) } else { return Ok(()); }; let (clamped_x, clamped_y) = clamp_origin(window, next_x, next_y, width, height)?; + if !force_default_position && height > current_size.height { + *expansion = Some((previous, (clamped_x, clamped_y))); + } if size_changed { window.set_size(Size::Logical(next_size))?; } @@ -536,6 +560,30 @@ mod tests { assert_eq!(layout::container_size(true, true), (368.0, 459.0)); } + #[test] + fn collapse_restores_position_after_expansion_was_clamped() { + let compact = (1801.0, 1005.0, 111.0, 67.0); + let expanded = layout::resize_anchored( + compact.0, compact.1, compact.2, compact.3, 368.0, 459.0, true, + ); + let clamped = layout::clamp_to_work_area( + expanded.0, expanded.1, 368.0, 459.0, 0.0, 0.0, 1920.0, 1080.0, + ); + let anchor = layout::collapse_anchor( + (clamped.0, clamped.1, 368.0, 459.0), + Some((compact, clamped)), + ); + assert_eq!( + layout::resize_anchored(anchor.0, anchor.1, anchor.2, anchor.3, 111.0, 67.0, true), + (compact.0, compact.1) + ); + let moved = (clamped.0 - 100.0, clamped.1, 368.0, 459.0); + assert_eq!( + layout::collapse_anchor(moved, Some((compact, clamped))), + moved + ); + } + #[test] fn pins_the_default_origin_to_the_work_area_bottom_center() { assert_eq!( diff --git a/plugins/windows/swift-lib/src/FloatingBarManager.swift b/plugins/windows/swift-lib/src/FloatingBarManager.swift index 3dedce96ef6..e81e342e16c 100644 --- a/plugins/windows/swift-lib/src/FloatingBarManager.swift +++ b/plugins/windows/swift-lib/src/FloatingBarManager.swift @@ -7,6 +7,7 @@ final class FloatingBarManager { private var panel: NSPanel? private var expandsUpward = true + private var expansion: (compact: NSRect, expanded: NSRect)? private let model = FloatingBarViewModel() private let settingsModel = FloatingOverlaySettingsModel.shared private let placement = FloatingPanelPositionController() @@ -72,6 +73,8 @@ final class FloatingBarManager { return } + expandsUpward = true + expansion = nil FloatingBarFonts.register() let panel = createPanel() @@ -82,6 +85,7 @@ final class FloatingBarManager { panelOrigin: { [weak self] in self?.panel?.frame.origin }, movePanel: { [weak self] origin in guard let self, let panel = self.panel else { return } + self.expansion = nil self.placement.moveByUserDrag( panel, to: origin, @@ -214,7 +218,13 @@ final class FloatingBarManager { } let nextAnchorOffset = controlAnchorOffset(for: nextLayout) let frame = FloatingBarPlacement.resizedFrame( - panel.frame, size: size, workArea: workArea, expandsUpward: expandsUpward) + panel.frame, size: size, workArea: workArea, expandsUpward: expandsUpward, + expansion: expansion) + if size.height > previousSize.height { + expansion = (panel.frame, frame) + } else { + expansion = nil + } placement.setFrame( panel, to: frame, diff --git a/plugins/windows/swift-lib/src/FloatingBarPlacement.swift b/plugins/windows/swift-lib/src/FloatingBarPlacement.swift index 37303932686..84da21ea369 100644 --- a/plugins/windows/swift-lib/src/FloatingBarPlacement.swift +++ b/plugins/windows/swift-lib/src/FloatingBarPlacement.swift @@ -6,8 +6,13 @@ enum FloatingBarPlacement { } static func resizedFrame( - _ frame: NSRect, size: NSSize, workArea: NSRect, expandsUpward: Bool + _ frame: NSRect, size: NSSize, workArea: NSRect, expandsUpward: Bool, + expansion: (compact: NSRect, expanded: NSRect)? = nil ) -> NSRect { + let frame = + expansion.flatMap { saved in + size.height < frame.height && frame == saved.expanded ? saved.compact : nil + } ?? frame let x = frame.midX - size.width / 2 let y = expandsUpward ? frame.minY : frame.maxY - size.height return NSRect( diff --git a/plugins/windows/swift-lib/tests/FloatingBarPlacementTests.swift b/plugins/windows/swift-lib/tests/FloatingBarPlacementTests.swift index 5230c9b7021..57cf8279907 100644 --- a/plugins/windows/swift-lib/tests/FloatingBarPlacementTests.swift +++ b/plugins/windows/swift-lib/tests/FloatingBarPlacementTests.swift @@ -38,5 +38,9 @@ final class FloatingBarPlacementTests: XCTestCase { frame, size: expandedSize, workArea: workArea, expandsUpward: true) XCTAssertEqual(expanded.maxX, workArea.maxX) XCTAssertTrue(workArea.contains(expanded)) + XCTAssertEqual( + FloatingBarPlacement.resizedFrame( + expanded, size: compactSize, workArea: workArea, expandsUpward: true, + expansion: (frame, expanded)), frame) } } From c0baf0651ac5ea4ca17db38db00d2df09e9ea930 Mon Sep 17 00:00:00 2001 From: ComputelessComputer <63365510+ComputelessComputer@users.noreply.github.com> Date: Thu, 17 Sep 2026 17:08:52 +0900 Subject: [PATCH 3/3] Forget stale placement after dragging the panel Invalidate the saved compact origin when Windows or Linux moves the expanded panel, including a drag away and back. --- plugins/windows/src/window/floating_bar.rs | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/plugins/windows/src/window/floating_bar.rs b/plugins/windows/src/window/floating_bar.rs index 05b89bd0564..db0f3edd16d 100644 --- a/plugins/windows/src/window/floating_bar.rs +++ b/plugins/windows/src/window/floating_bar.rs @@ -119,6 +119,18 @@ pub(crate) mod layout { y - work_y > work_y + work_height - y - height } + #[cfg(any(test, not(target_os = "macos")))] + pub fn forget_moved_expansion( + expansion: &mut Option<((f64, f64, f64, f64), (f64, f64))>, + position: (f64, f64), + ) { + if expansion.is_some_and(|(_, origin)| { + (position.0 - origin.0).abs() >= 0.5 || (position.1 - origin.1).abs() >= 0.5 + }) { + *expansion = None; + } + } + pub fn collapse_anchor( current: (f64, f64, f64, f64), expansion: Option<((f64, f64, f64, f64), (f64, f64))>, @@ -390,6 +402,16 @@ mod platform { .disable_drag_drop_handler() .build()?; + let moved_window = window.clone(); + window.on_window_event(move |event| { + if let tauri::WindowEvent::Moved(position) = event + && let Ok(scale) = moved_window.scale_factor() + && let Ok(mut expansion) = EXPANSION.try_lock() + { + let position = position.to_logical::(scale); + super::layout::forget_moved_expansion(&mut expansion, (position.x, position.y)); + } + }); crate::window::exclude_from_capture(&window); Ok(window) @@ -553,6 +575,18 @@ pub fn update_amplitude(amplitude: f64) -> Result<(), Error> { mod tests { use super::layout; + #[test] + fn dragging_away_and_back_does_not_restore_the_old_compact_frame() { + let mut expansion = Some(((10.0, 20.0, 111.0, 67.0), (0.0, 20.0))); + layout::forget_moved_expansion(&mut expansion, (0.0, 20.0)); + assert!(expansion.is_some()); + layout::forget_moved_expansion(&mut expansion, (100.0, 20.0)); + layout::forget_moved_expansion(&mut expansion, (0.0, 20.0)); + assert!(expansion.is_none()); + let current = (0.0, 20.0, 368.0, 459.0); + assert_eq!(layout::collapse_anchor(current, expansion), current); + } + #[test] fn sizes_the_compact_and_expanded_windows() { assert_eq!(layout::container_size(false, false), (84.0, 67.0));