Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion apps/desktop-tauri/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"identifier": "default",
"description": "Default permissions for the CodexBar desktop scaffold.",
"windows": ["main", "settings", "floatbar"],
"windows": ["main", "settings", "floatbar", "flyout"],
"permissions": [
"core:event:allow-listen",
"core:event:allow-unlisten",
Expand All @@ -10,6 +10,8 @@
"core:window:allow-toggle-maximize",
"core:window:allow-set-size",
"core:window:allow-start-dragging",
"core:window:allow-start-resize-dragging",
"core:window:default",
"core:webview:allow-set-webview-zoom",
"global-shortcut:allow-register",
"global-shortcut:allow-unregister"
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ pub struct SettingsSnapshot {
ui_language: &'static str,
theme: &'static str,
window_scale_percent: u16,
tray_scale_percent: u16,
claude_avoid_keychain_prompts: bool,
disable_keychain_access: bool,
provider_metrics: std::collections::HashMap<String, &'static str>,
Expand Down Expand Up @@ -479,6 +480,7 @@ impl From<Settings> for SettingsSnapshot {
ui_language: language_label(settings.ui_language),
theme: theme_label(settings.theme),
window_scale_percent: settings.window_scale_percent,
tray_scale_percent: settings.tray_scale_percent,
claude_avoid_keychain_prompts: avoid_keychain_prompts,
disable_keychain_access: settings.disable_keychain_access,
provider_metrics,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ pub fn reorder_providers(
settings.provider_order = codexbar::settings::normalize_provider_order(&ids);
settings.save().map_err(|e| e.to_string())?;
crate::tray_bridge::refresh_tray_presentation(&app);
// Notify open surfaces (tray flyout, pop-out window) so their provider grid
// and cards re-render in the new order immediately after a drag-reorder.
crate::events::emit_settings_changed(&app);
Ok(build_provider_summaries(&settings))
}

Expand Down
23 changes: 23 additions & 0 deletions apps/desktop-tauri/src-tauri/src/commands/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ pub struct SettingsUpdate {
pub ui_language: Option<String>,
pub theme: Option<String>,
pub window_scale_percent: Option<u16>,
pub tray_scale_percent: Option<u16>,
pub claude_avoid_keychain_prompts: Option<bool>,
pub disable_keychain_access: Option<bool>,
/// Map of provider CLI name → metric preference label.
Expand Down Expand Up @@ -148,6 +149,9 @@ impl SettingsUpdate {
if let Some(v) = self.window_scale_percent {
settings.window_scale_percent = codexbar::settings::clamp_window_scale_percent(v);
}
if let Some(v) = self.tray_scale_percent {
settings.tray_scale_percent = codexbar::settings::clamp_tray_scale_percent(v);
}
if let Some(v) = self.switcher_shows_icons {
settings.switcher_shows_icons = v;
}
Expand Down Expand Up @@ -347,4 +351,23 @@ mod tests {
.apply_display_settings(&mut settings);
assert_eq!(settings.window_scale_percent, 100);
}

#[test]
fn apply_display_settings_clamps_tray_scale_percent() {
let mut settings = Settings::default();

SettingsUpdate {
tray_scale_percent: Some(300),
..Default::default()
}
.apply_display_settings(&mut settings);
assert_eq!(settings.tray_scale_percent, 200);

SettingsUpdate {
tray_scale_percent: Some(50),
..Default::default()
}
.apply_display_settings(&mut settings);
assert_eq!(settings.tray_scale_percent, 100);
}
}
90 changes: 77 additions & 13 deletions apps/desktop-tauri/src-tauri/src/commands/surface.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,36 @@ pub fn set_surface_mode(

#[tauri::command]
pub fn dismiss_tray_panel(app: tauri::AppHandle) -> Result<(), String> {
crate::shell::hide_to_tray_if_current(&app, |mode| mode == SurfaceMode::TrayPanel).map(|_| ())
crate::shell::flyout_window::hide(&app)
}

/// Arm the gesture blur guard before a resize-grip drag or drag-reorder
/// gesture starts its Win32/OLE modal loop, so the transient
/// `Focused(false)` that loop produces doesn't auto-hide the flyout.
#[tauri::command]
pub fn begin_flyout_gesture(app: tauri::AppHandle) -> Result<(), String> {
let state = app
.try_state::<Mutex<AppState>>()
.ok_or_else(|| "app state unavailable".to_string())?;
state
.lock()
.map_err(|e| e.to_string())?
.begin_gesture_blur_guard(std::time::Instant::now());
Ok(())
}

/// Disarm the gesture blur guard when a gesture ends (mouseup / dragend),
/// so a genuine outside click can dismiss the flyout again immediately.
#[tauri::command]
pub fn end_flyout_gesture(app: tauri::AppHandle) -> Result<(), String> {
let state = app
.try_state::<Mutex<AppState>>()
.ok_or_else(|| "app state unavailable".to_string())?;
state
.lock()
.map_err(|e| e.to_string())?
.end_gesture_blur_guard();
Ok(())
}

/// Open (or focus) a detached Settings/About window.
Expand All @@ -30,26 +59,37 @@ pub async fn open_settings_window(app: tauri::AppHandle, tab: String) -> Result<
crate::shell::settings_window::open_or_focus(&app, &tab)
}

/// Open (or focus) the detached flyout ("Pop Out Dashboard") window.
///
/// Used by `PopOutPanel`'s "back to tray" action, which previously called
/// `set_surface_mode("trayPanel", ...)` on the shared window — now that the
/// flyout is its own window, that action opens it directly instead. Same
/// `async` requirement as `open_settings_window`: `WebviewWindowBuilder::build`
/// deadlocks inside synchronous Tauri commands on Windows.
#[tauri::command]
pub async fn open_flyout_window(app: tauri::AppHandle) -> Result<(), String> {
crate::shell::flyout_window::open_or_focus(&app, None)
}

/// Reveal the flyout window after the frontend's first layout pass. Called by
/// `useTrayPanelLayout` once content has been measured/auto-fit (or the
/// remembered fixed size re-applied), so Windows never shows a pre-measure
/// blank/backing frame.
///
/// No-ops when the flyout window doesn't exist — the `== TrayPanel` gate this
/// replaced was checking "is the flyout the thing we're currently showing?";
/// now that the flyout is its own window (not a state of `main`'s surface
/// machine), window-existence is the equivalent check.
#[tauri::command]
pub fn reveal_tray_panel_window(
app: tauri::AppHandle,
state: tauri::State<'_, Mutex<AppState>>,
) -> Result<(), String> {
use tauri::Manager;

if state
.lock()
.map_err(|e| e.to_string())?
.surface_machine
.current()
!= SurfaceMode::TrayPanel
{
let Some(window) = app.get_webview_window(crate::shell::flyout_window::FLYOUT_LABEL) else {
return Ok(());
}

let window = app
.get_webview_window("main")
.ok_or_else(|| "main window unavailable".to_string())?;
};
window.show().map_err(|e| e.to_string())?;
state
.lock()
Expand All @@ -67,6 +107,30 @@ pub fn close_settings_window(
crate::shell::settings_window::dismiss(&app, &window)
}

/// Persist a user-chosen size for the "Pop Out Dashboard" flyout window.
/// Only the size is stored (via a size-only `StoredSize` entry — no
/// fabricated `x`/`y`); the flyout is always re-anchored above the tray on
/// open. The frontend calls this on genuine user drag-resizes, not on its own
/// auto-fit resizes, so auto-fit sizes never freeze the panel.
#[tauri::command]
pub fn set_flyout_size(width: f64, height: f64) -> Result<(), String> {
let width = (width.round() as i64).clamp(1, i64::from(u32::MAX)) as u32;
let height = (height.round() as i64).clamp(1, i64::from(u32::MAX)) as u32;
crate::shell::flyout_window::save_stored_size(width, height);
Ok(())
}

/// Return the remembered flyout size, if the user has manually resized it.
/// The frontend uses this to decide whether to auto-fit (no stored size) or
/// honor the user's size (stored) on open. Transparently migrates a
/// pre-existing size stored under the legacy `SurfaceMode::TrayPanel`
/// shared-window geometry key (from before the flyout became its own
/// window), so upgrading users don't lose their remembered size.
#[tauri::command]
pub fn flyout_stored_size() -> Result<Option<(u32, u32)>, String> {
Ok(crate::shell::flyout_window::stored_size())
}

#[tauri::command]
pub fn get_current_surface_mode(
state: tauri::State<'_, Mutex<AppState>>,
Expand Down
72 changes: 8 additions & 64 deletions apps/desktop-tauri/src-tauri/src/commands/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,73 +188,17 @@ pub fn play_notification_sound() -> Result<(), String> {
Ok(())
}

/// Reposition the tray panel so its bottom-right corner stays anchored to
/// Reposition the flyout window so its bottom-right corner stays anchored to
/// the system-tray area. Called from the frontend after dynamic resize.
///
/// Retargeted from `main` to the dedicated `flyout` window — the flyout is no
/// longer a state of `main`'s surface-mode machine, so `reanchor_tray_panel`
/// (still exported under its historical name — the frontend command name is
/// unchanged) now anchors the flyout window directly. The anchor math itself
/// lives in `shell::flyout_window::reanchor`, which this delegates to.
#[tauri::command]
pub fn reanchor_tray_panel(app: tauri::AppHandle) -> Result<(), String> {
use crate::window_positioner::{PanelSize, Rect};
use tauri::Manager;

let window = app
.get_webview_window("main")
.ok_or_else(|| "main window unavailable".to_string())?;
let scale = window.scale_factor().unwrap_or(1.0).max(1.0);

// Use the window's current logical size (after JS resize).
let outer = window.outer_size().map_err(|e| e.to_string())?;
let panel_size = PanelSize {
width: (outer.width as f64 / scale).round() as u32,
height: (outer.height as f64 / scale).round() as u32,
};

// Prefer the saved tray anchor from a real click; otherwise infer one from
// the taskbar side.
let monitor = window
.primary_monitor()
.ok()
.flatten()
.or_else(|| window.current_monitor().ok().flatten())
.ok_or_else(|| "no monitor".to_string())?;

let work_area = Rect {
x: monitor.work_area().position.x,
y: monitor.work_area().position.y,
width: monitor.work_area().size.width,
height: monitor.work_area().size.height,
};

let (x, y) = {
let st = app.try_state::<std::sync::Mutex<crate::state::AppState>>();
let anchor = st.and_then(|s| s.lock().ok()?.tray_anchor);
if let Some(a) = anchor {
crate::window_positioner::calculate_panel_position(
&Rect {
x: a.x,
y: a.y,
width: a.width,
height: a.height,
},
&work_area,
&panel_size,
scale,
)
} else {
crate::shell::inferred_tray_panel_position_for_monitor_size(&monitor, &panel_size)
}
};

// Pass physical coordinates directly — tao converts PhysicalPosition
// to OS logical internally by dividing by the window's scale factor.
let pos = tauri::PhysicalPosition::new(x, y);
tracing::debug!(
"reanchor_tray_panel: panel={}x{} => ({},{})",
panel_size.width,
panel_size.height,
pos.x,
pos.y
);
let _ = window.set_position(pos);
Ok(())
crate::shell::flyout_window::reanchor(&app)
}

#[tauri::command]
Expand Down
Loading
Loading