diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 854ce119..92a0fd39 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -69,10 +69,7 @@ mod plugin; mod render; mod updates; mod window_menu; -#[cfg(target_os = "macos")] -mod mac; -#[cfg(target_os = "windows")] -mod win; +mod tauri_plugin_traffic_light; async fn migrate_db(app_handle: &AppHandle, db: &Mutex>) -> Result<(), String> { let pool = &*db.lock().await; @@ -1502,8 +1499,14 @@ async fn cmd_list_workspaces(w: WebviewWindow) -> Result, String> } #[tauri::command] -async fn cmd_new_window(window: WebviewWindow, url: &str) -> Result<(), String> { - create_window(&window.app_handle(), Some(url)); +async fn cmd_new_window(app_handle: AppHandle, url: &str) -> Result<(), String> { + create_window(&app_handle, url); + Ok(()) +} + +#[tauri::command] +async fn cmd_new_nested_window(window: WebviewWindow, url: &str, label: &str, title: &str) -> Result<(), String> { + create_nested_window(&window, label, url, title); Ok(()) } @@ -1532,12 +1535,13 @@ async fn cmd_check_for_updates( pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_clipboard_manager::init()) - .plugin(tauri_plugin_window_state::Builder::default().build()) + .plugin(tauri_plugin_window_state::Builder::default().with_denylist(&["settings"]).build()) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_fs::init()) + .plugin(tauri_plugin_traffic_light::init()) .plugin( tauri_plugin_log::Builder::default() .targets([ @@ -1663,6 +1667,7 @@ pub fn run() { cmd_list_http_responses, cmd_list_workspaces, cmd_metadata, + cmd_new_nested_window, cmd_new_window, cmd_request_to_curl, cmd_dismiss_notification, @@ -1691,7 +1696,7 @@ pub fn run() { .run(|app_handle, event| { match event { RunEvent::Ready => { - create_window(app_handle, None); + create_window(app_handle, "/"); let h = app_handle.clone(); tauri::async_runtime::spawn(async move { let info = analytics::track_launch_event(&h).await; @@ -1737,7 +1742,84 @@ fn is_dev() -> bool { } } -fn create_window(handle: &AppHandle, url: Option<&str>) -> WebviewWindow { +fn create_nested_window(window: &WebviewWindow, label: &str, url: &str, title: &str) -> WebviewWindow { + info!("Create new nested window label={label}"); + let pos = window.outer_position().unwrap(); + let mut win_builder = tauri::WebviewWindowBuilder::new( + window, + label, + WebviewUrl::App(url.into()), + ) + .resizable(true) + .fullscreen(false) + .disable_drag_drop_handler() // Required for frontend Dnd on windows + .title(title) + .parent(&window) + .unwrap() + .position( + (pos.x + 20) as f64, + (pos.y + 20) as f64, + ) + .inner_size( + 500.0f64, + 300.0f64, + ); + + // Add macOS-only things + #[cfg(target_os = "macos")] + { + win_builder = win_builder + .hidden_title(true) + .title_bar_style(TitleBarStyle::Overlay); + } + + // Add non-MacOS things + #[cfg(not(target_os = "macos"))] + { + win_builder = win_builder.decorations(false); + } + + let win = win_builder.build().expect("failed to build window"); + + // Tauri doesn't support shadows when hiding decorations, so we add our own + // #[cfg(any(windows, target_os = "macos"))] + // set_shadow(&win, true).unwrap(); + + let win2 = win.clone(); + win.on_menu_event(move |w, event| { + if !w.is_focused().unwrap() { + return; + } + + match event.id().0.as_str() { + "quit" => exit(0), + "close" => _ = w.close(), + "zoom_reset" => w.emit("zoom_reset", true).unwrap(), + "zoom_in" => w.emit("zoom_in", true).unwrap(), + "zoom_out" => w.emit("zoom_out", true).unwrap(), + "settings" => w.emit("settings", true).unwrap(), + "refresh" => win2.eval("location.reload()").unwrap(), + "open_feedback" => { + _ = win2 + .app_handle() + .shell() + .open("https://yaak.canny.io", None) + } + "toggle_devtools" => { + if win2.is_devtools_open() { + win2.close_devtools(); + } else { + win2.open_devtools(); + } + } + _ => {} + } + }); + + win +} + +fn create_window(handle: &AppHandle, url: &str) -> WebviewWindow { let menu = app_menu(handle).unwrap(); // This causes the window to not be clickable (in AppImage), so disable on Linux @@ -1745,11 +1827,12 @@ fn create_window(handle: &AppHandle, url: Option<&str>) -> WebviewWindow { handle.set_menu(menu).expect("Failed to set app menu"); let window_num = handle.webview_windows().len(); - let window_id = format!("wnd_{}", window_num); + let label = format!("wnd_{}", window_num); + info!("Create new window label={label}"); let mut win_builder = tauri::WebviewWindowBuilder::new( handle, - window_id, - WebviewUrl::App(url.unwrap_or_default().into()), + label, + WebviewUrl::App(url.into()), ) .resizable(true) .fullscreen(false) @@ -1774,16 +1857,12 @@ fn create_window(handle: &AppHandle, url: Option<&str>) -> WebviewWindow { // Add non-MacOS things #[cfg(not(target_os = "macos"))] { - // Doesn't seem to work from Rust, here, so we do it in JS - win_builder = win_builder.decorations(false); + // Doesn't seem to work from Rust, here, so we do it in main.tsx + // win_builder = win_builder.decorations(false); } let win = win_builder.build().expect("failed to build window"); - // Tauri doesn't support shadows when hiding decorations, so we add our own - // #[cfg(any(windows, target_os = "macos"))] - // set_shadow(&win, true).unwrap(); - let win2 = win.clone(); win.on_menu_event(move |w, event| { if !w.is_focused().unwrap() { @@ -1792,12 +1871,11 @@ fn create_window(handle: &AppHandle, url: Option<&str>) -> WebviewWindow { match event.id().0.as_str() { "quit" => exit(0), - "close" => w.close().unwrap(), + "close" => _ = w.close(), "zoom_reset" => w.emit("zoom_reset", true).unwrap(), "zoom_in" => w.emit("zoom_in", true).unwrap(), "zoom_out" => w.emit("zoom_out", true).unwrap(), "settings" => w.emit("settings", true).unwrap(), - "duplicate_request" => w.emit("duplicate_request", true).unwrap(), "refresh" => win2.eval("location.reload()").unwrap(), "open_feedback" => { _ = win2 @@ -1816,19 +1894,6 @@ fn create_window(handle: &AppHandle, url: Option<&str>) -> WebviewWindow { } }); - #[cfg(target_os = "macos")] - { - use mac::setup_mac_window; - let mut m_win = win.clone(); - setup_mac_window(&mut m_win); - }; - #[cfg(target_os = "windows")] - { - use win::setup_win_window; - let mut m_win = win.clone(); - setup_win_window(&mut m_win); - } - win } diff --git a/src-tauri/src/mac.rs b/src-tauri/src/tauri_plugin_traffic_light.rs similarity index 59% rename from src-tauri/src/mac.rs rename to src-tauri/src/tauri_plugin_traffic_light.rs index ef31c65c..581f1aef 100644 --- a/src-tauri/src/mac.rs +++ b/src-tauri/src/tauri_plugin_traffic_light.rs @@ -1,89 +1,44 @@ -// Borrowed from our friends at Hoppscotch -// https://github.com/hoppscotch/hoppscotch/blob/286fcd2bb08a84f027b10308d1e18da368f95ebf/packages/hoppscotch-selfhost-desktop/src-tauri/src/mac/window.rs - -use hex_color::HexColor; -use tauri::{Manager, WebviewWindow}; - -struct UnsafeWindowHandle(*mut std::ffi::c_void); - -unsafe impl Send for UnsafeWindowHandle {} - -unsafe impl Sync for UnsafeWindowHandle {} +use objc::{msg_send, sel, sel_impl}; +use rand::{distributions::Alphanumeric, Rng}; +use tauri::{Manager, plugin::{Builder, TauriPlugin}, Runtime, Window}; const WINDOW_CONTROL_PAD_X: f64 = 13.0; const WINDOW_CONTROL_PAD_Y: f64 = 18.0; -#[cfg(target_os = "macos")] -fn update_window_title(window: &WebviewWindow, title: String) { - use cocoa::{ - appkit::NSWindow, - base::nil, - foundation::NSString, - }; +struct UnsafeWindowHandle(*mut std::ffi::c_void); +unsafe impl Send for UnsafeWindowHandle {} +unsafe impl Sync for UnsafeWindowHandle {} - unsafe { - let window_handle = UnsafeWindowHandle(window.ns_window().unwrap()); - - let _ = window.run_on_main_thread(move || { - let win_title = NSString::alloc(nil).init_str(&title); - let handle = window_handle; - NSWindow::setTitle_(handle.0 as cocoa::base::id, win_title); - set_window_controls_pos(handle.0 as cocoa::base::id); - }); - } +pub fn init() -> TauriPlugin { + Builder::new("traffic_light_positioner") + .on_window_ready(|window| { + #[cfg(target_os = "macos")] + setup_traffic_light_positioner(window); + return; + }) + .build() } #[cfg(target_os = "macos")] -fn update_window_theme(window: &WebviewWindow, color: HexColor) { - use cocoa::{ - appkit::{NSAppearance, NSAppearanceNameVibrantDark, NSAppearanceNameVibrantLight, NSWindow}, - base::nil, - foundation::NSString, - }; - - let brightness = (color.r as u64 + color.g as u64 + color.b as u64) / 3; - +fn position_traffic_lights(ns_window_handle: UnsafeWindowHandle, x: f64, y: f64) { + use cocoa::appkit::{NSView, NSWindow, NSWindowButton}; + use cocoa::foundation::NSRect; + let ns_window = ns_window_handle.0 as cocoa::base::id; unsafe { - let window_handle = UnsafeWindowHandle(window.ns_window().unwrap()); - - let _ = window.run_on_main_thread(move || { - let handle = window_handle; - - let selected_appearance = if brightness >= 128 { - NSAppearance(NSAppearanceNameVibrantLight) - } else { - NSAppearance(NSAppearanceNameVibrantDark) - }; - - let title = NSString::alloc(nil).init_str("My Title"); - NSWindow::setTitle_(handle.0 as cocoa::base::id, title); - NSWindow::setAppearance(handle.0 as cocoa::base::id, selected_appearance); - set_window_controls_pos(handle.0 as cocoa::base::id); - }); - } -} - -#[cfg(target_os = "macos")] -fn set_window_controls_pos(window: cocoa::base::id) { - use cocoa::{ - appkit::{NSView, NSWindow, NSWindowButton}, - foundation::NSRect, - }; - - unsafe { - let close = window.standardWindowButton_(NSWindowButton::NSWindowCloseButton); - let miniaturize = window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton); - let zoom = window.standardWindowButton_(NSWindowButton::NSWindowZoomButton); + let close = ns_window.standardWindowButton_(NSWindowButton::NSWindowCloseButton); + let miniaturize = + ns_window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton); + let zoom = ns_window.standardWindowButton_(NSWindowButton::NSWindowZoomButton); let title_bar_container_view = close.superview().superview(); let close_rect: NSRect = msg_send![close, frame]; let button_height = close_rect.size.height; - let title_bar_frame_height = button_height + WINDOW_CONTROL_PAD_Y; + let title_bar_frame_height = button_height + y; let mut title_bar_rect = NSView::frame(title_bar_container_view); title_bar_rect.size.height = title_bar_frame_height; - title_bar_rect.origin.y = NSView::frame(window).size.height - title_bar_frame_height; + title_bar_rect.origin.y = NSView::frame(ns_window).size.height - title_bar_frame_height; let _: () = msg_send![title_bar_container_view, setFrame: title_bar_rect]; let window_buttons = vec![close, miniaturize, zoom]; @@ -91,7 +46,7 @@ fn set_window_controls_pos(window: cocoa::base::id) { for (i, button) in window_buttons.into_iter().enumerate() { let mut rect: NSRect = NSView::frame(button); - rect.origin.x = WINDOW_CONTROL_PAD_X + (i as f64 * space_between); + rect.origin.x = x + (i as f64 * space_between); button.setFrameOrigin(rect.origin); } } @@ -99,12 +54,12 @@ fn set_window_controls_pos(window: cocoa::base::id) { #[cfg(target_os = "macos")] #[derive(Debug)] -struct AppState { - window: WebviewWindow, +struct WindowState { + window: Window, } #[cfg(target_os = "macos")] -pub fn setup_mac_window(window: &mut WebviewWindow) { +pub fn setup_traffic_light_positioner(window: Window) { use cocoa::delegate; use cocoa::appkit::NSWindow; use cocoa::base::{BOOL, id}; @@ -112,16 +67,30 @@ pub fn setup_mac_window(window: &mut WebviewWindow) { use objc::runtime::{Object, Sel}; use std::ffi::c_void; - fn with_app_state T, T>(this: &Object, func: F) { + // Do the initial positioning + position_traffic_lights( + UnsafeWindowHandle(window.ns_window().expect("Failed to create window handle")), + WINDOW_CONTROL_PAD_X, + WINDOW_CONTROL_PAD_Y, + ); + + // Ensure they stay in place while resizing the window. + fn with_window_state) -> T, T>( + this: &Object, + func: F, + ) { let ptr = unsafe { - let x: *mut c_void = *this.get_ivar("yaakApp"); - &mut *(x as *mut AppState) + let x: *mut c_void = *this.get_ivar("app_box"); + &mut *(x as *mut WindowState) }; func(ptr); } unsafe { - let ns_win = window.ns_window().unwrap() as id; + let ns_win = window + .ns_window() + .expect("NS Window should exist to mount traffic light delegate.") + as id; let current_delegate: id = ns_win.delegate(); @@ -137,12 +106,21 @@ pub fn setup_mac_window(window: &mut WebviewWindow) { let _: () = msg_send![super_del, windowWillClose: notification]; } } - extern "C" fn on_window_did_resize(this: &Object, _cmd: Sel, notification: id) { + extern "C" fn on_window_did_resize(this: &Object, _cmd: Sel, notification: id) { unsafe { - with_app_state(&*this, |state| { - let id = state.window.ns_window().unwrap() as id; + with_window_state(&*this, |state: &mut WindowState| { + let id = state + .window + .ns_window() + .expect("NS window should exist on state to handle resize") + as id; - set_window_controls_pos(id); + #[cfg(target_os = "macos")] + position_traffic_lights( + UnsafeWindowHandle(id as *mut std::ffi::c_void), + WINDOW_CONTROL_PAD_X, + WINDOW_CONTROL_PAD_Y, + ); }); let super_del: id = *this.get_ivar("super_delegate"); @@ -222,43 +200,75 @@ pub fn setup_mac_window(window: &mut WebviewWindow) { msg_send![super_del, window: window willUseFullScreenPresentationOptions: proposed_options] } } - extern "C" fn on_window_did_enter_full_screen(this: &Object, _cmd: Sel, notification: id) { + extern "C" fn on_window_did_enter_full_screen( + this: &Object, + _cmd: Sel, + notification: id, + ) { unsafe { - with_app_state(&*this, |state| { - state.window.emit("did-enter-fullscreen", ()).unwrap(); + with_window_state(&*this, |state: &mut WindowState| { + state + .window + .emit("did-enter-fullscreen", ()) + .expect("Failed to emit event"); }); let super_del: id = *this.get_ivar("super_delegate"); let _: () = msg_send![super_del, windowDidEnterFullScreen: notification]; } } - extern "C" fn on_window_will_enter_full_screen(this: &Object, _cmd: Sel, notification: id) { + extern "C" fn on_window_will_enter_full_screen( + this: &Object, + _cmd: Sel, + notification: id, + ) { unsafe { - with_app_state(&*this, |state| { - state.window.emit("will-enter-fullscreen", ()).unwrap(); + with_window_state(&*this, |state: &mut WindowState| { + state + .window + .emit("will-enter-fullscreen", ()) + .expect("Failed to emit event"); }); let super_del: id = *this.get_ivar("super_delegate"); let _: () = msg_send![super_del, windowWillEnterFullScreen: notification]; } } - extern "C" fn on_window_did_exit_full_screen(this: &Object, _cmd: Sel, notification: id) { + extern "C" fn on_window_did_exit_full_screen( + this: &Object, + _cmd: Sel, + notification: id, + ) { unsafe { - with_app_state(&*this, |state| { - state.window.emit("did-exit-fullscreen", ()).unwrap(); + with_window_state(&*this, |state: &mut WindowState| { + state + .window + .emit("did-exit-fullscreen", ()) + .expect("Failed to emit event"); - let id = state.window.ns_window().unwrap() as id; - set_window_controls_pos(id); + let id = state.window.ns_window().expect("Failed to emit event") as id; + position_traffic_lights( + UnsafeWindowHandle(id as *mut std::ffi::c_void), + WINDOW_CONTROL_PAD_X, + WINDOW_CONTROL_PAD_Y, + ); }); let super_del: id = *this.get_ivar("super_delegate"); let _: () = msg_send![super_del, windowDidExitFullScreen: notification]; } } - extern "C" fn on_window_will_exit_full_screen(this: &Object, _cmd: Sel, notification: id) { + extern "C" fn on_window_will_exit_full_screen( + this: &Object, + _cmd: Sel, + notification: id, + ) { unsafe { - with_app_state(&*this, |state| { - state.window.emit("will-exit-fullscreen", ()).unwrap(); + with_window_state(&*this, |state: &mut WindowState| { + state + .window + .emit("will-exit-fullscreen", ()) + .expect("Failed to emit event"); }); let super_del: id = *this.get_ivar("super_delegate"); @@ -299,44 +309,29 @@ pub fn setup_mac_window(window: &mut WebviewWindow) { } } - // extern fn on_dealloc(this: &Object, cmd: Sel) { - // unsafe { - // let super_del: id = *this.get_ivar("super_delegate"); - // let _: () = msg_send![super_del, dealloc]; - // } - // } - - // extern fn on_mark_is_checking_zoomed_in(this: &Object, cmd: Sel) { - // unsafe { - // let super_del: id = *this.get_ivar("super_delegate"); - // let _: () = msg_send![super_del, markIsCheckingZoomedIn]; - // } - // } - - // extern fn on_clear_is_checking_zoomed_in(this: &Object, cmd: Sel) { - // unsafe { - // let super_del: id = *this.get_ivar("super_delegate"); - // let _: () = msg_send![super_del, clearIsCheckingZoomedIn]; - // } - // } - // Are we deallocing this properly ? (I miss safe Rust :( ) - let w = window.clone(); - let app_state = AppState { window: w }; - let app_box = Box::into_raw(Box::new(app_state)) as *mut c_void; - set_window_controls_pos(ns_win); + let window_label = window.label().to_string(); - ns_win.setDelegate_(delegate!("MainWindowDelegate", { + let app_state = WindowState { window }; + let app_box = Box::into_raw(Box::new(app_state)) as *mut c_void; + let random_str: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(20) + .map(char::from) + .collect(); + + // We need to ensure we have a unique delegate name, otherwise we will panic while trying to create a duplicate + // delegate with the same name. + let delegate_name = format!("windowDelegate_{}_{}", window_label, random_str); + + ns_win.setDelegate_(delegate!(&delegate_name, { window: id = ns_win, - yaakApp: *mut c_void = app_box, + app_box: *mut c_void = app_box, toolbar: id = cocoa::base::nil, super_delegate: id = current_delegate, - // (dealloc) => on_dealloc as extern fn(&Object, Sel), - // (markIsCheckingZoomedIn) => on_mark_is_checking_zoomed_in as extern fn(&Object, Sel), - // (clearIsCheckingZoomedIn) => on_clear_is_checking_zoomed_in as extern fn(&Object, Sel), (windowShouldClose:) => on_window_should_close as extern fn(&Object, Sel, id) -> BOOL, (windowWillClose:) => on_window_will_close as extern fn(&Object, Sel, id), - (windowDidResize:) => on_window_did_resize as extern fn(&Object, Sel, id), + (windowDidResize:) => on_window_did_resize:: as extern fn(&Object, Sel, id), (windowDidMove:) => on_window_did_move as extern fn(&Object, Sel, id), (windowDidChangeBackingProperties:) => on_window_did_change_backing_properties as extern fn(&Object, Sel, id), (windowDidBecomeKey:) => on_window_did_become_key as extern fn(&Object, Sel, id), @@ -347,36 +342,13 @@ pub fn setup_mac_window(window: &mut WebviewWindow) { (concludeDragOperation:) => on_conclude_drag_operation as extern fn(&Object, Sel, id), (draggingExited:) => on_dragging_exited as extern fn(&Object, Sel, id), (window:willUseFullScreenPresentationOptions:) => on_window_will_use_full_screen_presentation_options as extern fn(&Object, Sel, id, NSUInteger) -> NSUInteger, - (windowDidEnterFullScreen:) => on_window_did_enter_full_screen as extern fn(&Object, Sel, id), - (windowWillEnterFullScreen:) => on_window_will_enter_full_screen as extern fn(&Object, Sel, id), - (windowDidExitFullScreen:) => on_window_did_exit_full_screen as extern fn(&Object, Sel, id), - (windowWillExitFullScreen:) => on_window_will_exit_full_screen as extern fn(&Object, Sel, id), + (windowDidEnterFullScreen:) => on_window_did_enter_full_screen:: as extern fn(&Object, Sel, id), + (windowWillEnterFullScreen:) => on_window_will_enter_full_screen:: as extern fn(&Object, Sel, id), + (windowDidExitFullScreen:) => on_window_did_exit_full_screen:: as extern fn(&Object, Sel, id), + (windowWillExitFullScreen:) => on_window_will_exit_full_screen:: as extern fn(&Object, Sel, id), (windowDidFailToEnterFullScreen:) => on_window_did_fail_to_enter_full_screen as extern fn(&Object, Sel, id), (effectiveAppearanceDidChange:) => on_effective_appearance_did_change as extern fn(&Object, Sel, id), (effectiveAppearanceDidChangedOnMainThread:) => on_effective_appearance_did_changed_on_main_thread as extern fn(&Object, Sel, id) })) } - - let app = window.app_handle(); - let window = window.clone(); - - // Control window theme based on app update_window - let window_for_theme = window.clone(); - app.listen_any("yaak_bg_changed", move |ev| { - let payload = serde_json::from_str::<&str>(ev.payload()) - .unwrap() - .trim(); - - let color = HexColor::parse_rgb(payload).unwrap(); - - update_window_theme(&window_for_theme, color); - }); - - let window_for_title = window.clone(); - app.listen_any("yaak_title_changed", move |ev| { - let title = serde_json::from_str::<&str>(ev.payload()) - .unwrap() - .trim(); - update_window_title(&window_for_title, title.to_string()); - }); } diff --git a/src-tauri/src/updates.rs b/src-tauri/src/updates.rs index 58611ab3..5e5a92a9 100644 --- a/src-tauri/src/updates.rs +++ b/src-tauri/src/updates.rs @@ -19,6 +19,7 @@ pub struct YaakUpdater { pub enum UpdateMode { Stable, Beta, + Alpha, } impl Display for UpdateMode { @@ -26,6 +27,7 @@ impl Display for UpdateMode { let s = match self { UpdateMode::Stable => "stable", UpdateMode::Beta => "beta", + UpdateMode::Alpha => "alpha", }; write!(f, "{}", s) } @@ -35,6 +37,7 @@ impl UpdateMode { pub fn new(mode: &str) -> UpdateMode { match mode { "beta" => UpdateMode::Beta, + "alpha" => UpdateMode::Alpha, _ => UpdateMode::Stable, } } diff --git a/src-tauri/src/win.rs b/src-tauri/src/win.rs deleted file mode 100644 index 70f763ec..00000000 --- a/src-tauri/src/win.rs +++ /dev/null @@ -1,79 +0,0 @@ -// Borrowed from our friends at Hoppscotch -// https://github.com/hoppscotch/hoppscotch/blob/286fcd2bb08a84f027b10308d1e18da368f95ebf/packages/hoppscotch-selfhost-desktop/src-tauri/src/mac/window.rs -use std::mem::transmute; - -use hex_color::HexColor; - -use windows::Win32::Foundation::BOOL; -use windows::Win32::Foundation::COLORREF; -use windows::Win32::Foundation::HWND; -use windows::Win32::Graphics::Dwm::DwmSetWindowAttribute; -use windows::Win32::Graphics::Dwm::DWMWA_CAPTION_COLOR; -use windows::Win32::Graphics::Dwm::DWMWA_USE_IMMERSIVE_DARK_MODE; -use windows::Win32::UI::Controls::{WTA_NONCLIENT, WTNCA_NODRAWICON, WTNCA_NOMIRRORHELP, WTNCA_NOSYSMENU}; -use windows::Win32::UI::Controls::SetWindowThemeAttribute; -use windows::Win32::UI::Controls::WTNCA_NODRAWCAPTION; - -fn hex_color_to_colorref(color: HexColor) -> COLORREF { - // TODO: Remove this unsafe, This operation doesn't need to be unsafe! - unsafe { - COLORREF(transmute::<[u8; 4], u32>([color.r, color.g, color.b, 0])) - } -} - -struct WinThemeAttribute { - flag: u32, - mask: u32 -} - -#[cfg(target_os = "windows")] -fn update_bg_color(hwnd: &HWND, bg_color: HexColor) { - - let use_dark_mode = BOOL::from(true); - - let final_color = hex_color_to_colorref(bg_color); - - unsafe { - DwmSetWindowAttribute( - HWND(hwnd.0), - DWMWA_USE_IMMERSIVE_DARK_MODE, - ptr::addr_of!(use_dark_mode) as *const c_void, - size_of::().try_into().unwrap() - ).unwrap(); - - DwmSetWindowAttribute( - HWND(hwnd.0), - DWMWA_CAPTION_COLOR, - ptr::addr_of!(final_color) as *const c_void, - size_of::().try_into().unwrap() - ).unwrap(); - - let flags = WTNCA_NODRAWCAPTION | WTNCA_NODRAWICON; - let mask = WTNCA_NODRAWCAPTION | WTNCA_NODRAWICON | WTNCA_NOSYSMENU | WTNCA_NOMIRRORHELP; - let options = WinThemeAttribute { flag: flags, mask }; - - SetWindowThemeAttribute( - HWND(hwnd.0), - WTA_NONCLIENT, - ptr::addr_of!(options) as *const c_void, - size_of::().try_into().unwrap() - ).unwrap(); - } -} - -#[cfg(target_os = "windows")] -pub fn setup_win_window(window: &mut WebviewWindow) { - let win_handle = window.hwnd().unwrap(); - let win_clone = win_handle.clone(); - - window.listen_global("yaak_bg_changed", move |ev| { - let payload = serde_json::from_str::<&str>(ev.payload().unwrap()) - .unwrap() - .trim(); - - let color = HexColor::parse_rgb(payload).unwrap(); - update_bg_color(&HWND(win_clone.0), color); - }); - - update_bg_color(&HWND(win_handle.0), HexColor::rgb(23, 23, 23)); -} diff --git a/src-web/components/AppRouter.tsx b/src-web/components/AppRouter.tsx index 40939be4..66adfed8 100644 --- a/src-web/components/AppRouter.tsx +++ b/src-web/components/AppRouter.tsx @@ -3,6 +3,7 @@ import { routePaths, useAppRoutes } from '../hooks/useAppRoutes'; import { DefaultLayout } from './DefaultLayout'; import { RedirectToLatestWorkspace } from './RedirectToLatestWorkspace'; import RouteError from './RouteError'; +import { SettingsDialog } from './Settings/SettingsDialog'; import Workspace from './Workspace'; const router = createBrowserRouter([ @@ -36,6 +37,12 @@ const router = createBrowserRouter([ path: '/workspaces/:workspaceId/environments/:environmentId/requests/:requestId', element: , }, + { + path: routePaths.workspaceSettings({ + workspaceId: ':workspaceId', + }), + element: , + }, ], }, ]); diff --git a/src-web/components/GlobalHooks.tsx b/src-web/components/GlobalHooks.tsx index 0f3de624..d3a7b502 100644 --- a/src-web/components/GlobalHooks.tsx +++ b/src-web/components/GlobalHooks.tsx @@ -21,7 +21,6 @@ import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; import { settingsQueryKey, useSettings } from '../hooks/useSettings'; import { useSyncThemeToDocument } from '../hooks/useSyncThemeToDocument'; import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle'; -import { useUpdateSettings } from '../hooks/useUpdateSettings'; import { workspacesQueryKey } from '../hooks/useWorkspaces'; import { useZoom } from '../hooks/useZoom'; import type { Model } from '../lib/models'; @@ -140,7 +139,6 @@ export function GlobalHooks() { `--editor-font-size: ${editorFontSize}px`, ].join('; '); }, [settings]); - const updateSettings = useUpdateSettings(); // Handle Zoom. Note, Mac handles it in app menu, so need to also handle keyboard // shortcuts for Windows/Linux @@ -149,21 +147,8 @@ export function GlobalHooks() { useListenToTauriEvent('zoom_in', () => zoom.zoomIn); useHotKey('app.zoom_out', () => zoom.zoomOut); useListenToTauriEvent('zoom_out', () => zoom.zoomOut); - useHotKey('app.zoom_out', () => zoom.zoomReset); - useListenToTauriEvent('zoom_out', () => zoom.zoomReset); - - useHotKey('app.zoom_out', () => { - if (!settings) return; - updateSettings.mutate({ - ...settings, - interfaceScale: Math.max(0.4, settings.interfaceScale * 0.9), - }); - }); - - useHotKey('app.zoom_reset', () => { - if (!settings) return; - updateSettings.mutate({ ...settings, interfaceScale: 1 }); - }); + useHotKey('app.zoom_reset', () => zoom.zoomReset); + useListenToTauriEvent('zoom_reset', () => zoom.zoomReset); return null; } diff --git a/src-web/components/Settings/SettingsAppearance.tsx b/src-web/components/Settings/SettingsAppearance.tsx index 2caf03ea..7a06f2d3 100644 --- a/src-web/components/Settings/SettingsAppearance.tsx +++ b/src-web/components/Settings/SettingsAppearance.tsx @@ -1,4 +1,3 @@ -import { invoke } from '@tauri-apps/api/core'; import React from 'react'; import { useActiveWorkspace } from '../../hooks/useActiveWorkspace'; import { useResolvedAppearance } from '../../hooks/useResolvedAppearance'; @@ -10,10 +9,8 @@ import { trackEvent } from '../../lib/analytics'; import { clamp } from '../../lib/clamp'; import { isThemeDark } from '../../lib/theme/window'; import type { ButtonProps } from '../core/Button'; -import { Button } from '../core/Button'; import { Editor } from '../core/Editor'; import type { IconProps } from '../core/Icon'; -import { Icon } from '../core/Icon'; import { IconButton } from '../core/IconButton'; import { PlainInput } from '../core/PlainInput'; import type { SelectOption } from '../core/Select'; @@ -83,6 +80,7 @@ export function SettingsAppearance() { name="interfaceFontSize" label="Font Size" placeholder="16" + step={0.5} type="number" labelPosition="left" defaultValue={`${settings.interfaceFontSize}`} @@ -99,6 +97,7 @@ export function SettingsAppearance() { name="editorFontSize" label="Editor Font Size" placeholder="14" + step={0.5} type="number" labelPosition="left" defaultValue={`${settings.editorFontSize}`} @@ -124,35 +123,40 @@ export function SettingsAppearance() { trackEvent('setting', 'update', { appearance }); }} options={[ - { label: 'Sync with OS', value: 'system' }, + { label: 'Automatic', value: 'system' }, { label: 'Light', value: 'light' }, { label: 'Dark', value: 'dark' }, ]} /> - { - await updateSettings.mutateAsync({ ...settings, themeDark }); - trackEvent('setting', 'update', { themeDark }); - }} - /> + {(settings.appearance === 'system' || settings.appearance === 'light') && ( + { + await updateSettings.mutateAsync({ ...settings, themeDark }); + trackEvent('setting', 'update', { themeDark }); + }} + /> + )} + - ); } diff --git a/src-web/components/Settings/SettingsDialog.tsx b/src-web/components/Settings/SettingsDialog.tsx index 811b9a29..ac347df6 100644 --- a/src-web/components/Settings/SettingsDialog.tsx +++ b/src-web/components/Settings/SettingsDialog.tsx @@ -18,13 +18,25 @@ enum Tab { const tabs = [Tab.General, Tab.Appearance, Tab.Design]; const useTabState = createGlobalState(tabs[0]!); -export const SettingsDialog = () => { +interface Props { + fullscreen?: true; +} + +export const SettingsDialog = ({ fullscreen }: Props) => { const [tab, setTab] = useTabState(); const appInfo = useAppInfo(); const isDev = appInfo?.isDev ?? false; return ( -
+
+ {fullscreen && ( +
+ Settings +
+ )} (null); const dialog = useDialog(); const checkForUpdates = useCheckForUpdates(); + const routes = useAppRoutes(); + const workspaceId = useActiveWorkspaceId(); const showSettings = () => { dialog.show({ @@ -58,6 +63,20 @@ export function SettingsDropdown() { }); }, }, + { + key: 'foo', + label: 'Foo', + hotKeyAction: 'hotkeys.showHelp', + leftSlot: , + onSelect: async () => { + if (!workspaceId) return; + await invoke('cmd_new_nested_window', { + url: routes.paths.workspaceSettings({ workspaceId }), + label: 'settings', + title: 'Yaak Settings', + }); + }, + }, { key: 'import-data', label: 'Import Data', diff --git a/src-web/components/core/PlainInput.tsx b/src-web/components/core/PlainInput.tsx index 5767b38f..f250793f 100644 --- a/src-web/components/core/PlainInput.tsx +++ b/src-web/components/core/PlainInput.tsx @@ -7,6 +7,7 @@ import { HStack } from './Stacks'; export type PlainInputProps = Omit & { type: 'text' | 'password' | 'number'; + step?: number; }; export const PlainInput = forwardRef(function Input( diff --git a/src-web/hooks/useAppRoutes.ts b/src-web/hooks/useAppRoutes.ts index 07110e83..db45d1a4 100644 --- a/src-web/hooks/useAppRoutes.ts +++ b/src-web/hooks/useAppRoutes.ts @@ -1,9 +1,9 @@ -import { useNavigate } from 'react-router-dom'; -import { QUERY_ENVIRONMENT_ID } from './useActiveEnvironmentId'; -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; -import { useActiveRequestId } from './useActiveRequestId'; -import type { Environment } from '../lib/models'; import { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import type { Environment } from '../lib/models'; +import { QUERY_ENVIRONMENT_ID } from './useActiveEnvironmentId'; +import { useActiveRequestId } from './useActiveRequestId'; +import { useActiveWorkspaceId } from './useActiveWorkspaceId'; export type RouteParamsWorkspace = { workspaceId: string; @@ -18,6 +18,9 @@ export const routePaths = { workspaces() { return '/workspaces'; }, + workspaceSettings({ workspaceId } = { workspaceId: ':workspaceId' } as RouteParamsWorkspace) { + return `/workspaces/${workspaceId}/settings`; + }, workspace( { workspaceId, environmentId } = { workspaceId: ':workspaceId', diff --git a/src-web/hooks/useSettings.ts b/src-web/hooks/useSettings.ts index 47ba7b85..68b99bdb 100644 --- a/src-web/hooks/useSettings.ts +++ b/src-web/hooks/useSettings.ts @@ -12,6 +12,7 @@ export function useSettings() { queryKey: settingsQueryKey(), queryFn: async () => { const settings = (await invoke('cmd_get_settings')) as Settings; + console.log('SETTINGS', settings); return [settings]; }, }).data?.[0] ?? undefined diff --git a/src-web/hooks/useSyncWindowTitle.ts b/src-web/hooks/useSyncWindowTitle.ts index 290a831d..29b3dfa6 100644 --- a/src-web/hooks/useSyncWindowTitle.ts +++ b/src-web/hooks/useSyncWindowTitle.ts @@ -1,4 +1,3 @@ -import { emit } from '@tauri-apps/api/event'; import { getCurrent } from '@tauri-apps/api/webviewWindow'; import { useEffect } from 'react'; import { fallbackRequestName } from '../lib/fallbackRequestName'; @@ -12,7 +11,12 @@ export function useSyncWindowTitle() { const activeWorkspace = useActiveWorkspace(); const activeEnvironment = useActiveEnvironment(); const osInfo = useOsInfo(); + useEffect(() => { + if (osInfo?.osType == null) { + return; + } + let newTitle = activeWorkspace ? activeWorkspace.name : 'Yaak'; if (activeEnvironment) { newTitle += ` [${activeEnvironment.name}]`; @@ -25,9 +29,10 @@ export function useSyncWindowTitle() { // TODO: This resets the stoplight position so we can't use it on macOS yet. Perhaps // we can if (osInfo?.osType !== 'macos') { + console.log('DO IT', osInfo?.osType); getCurrent().setTitle(newTitle).catch(console.error); } else { - emit('yaak_title_changed', newTitle).catch(console.error); + // emit('yaak_title_changed', newTitle).catch(console.error); } }, [activeEnvironment, activeRequest, activeWorkspace, osInfo?.osType]); } diff --git a/src-web/lib/theme/themes.ts b/src-web/lib/theme/themes.ts index 6a84fa50..55c52f96 100644 --- a/src-web/lib/theme/themes.ts +++ b/src-web/lib/theme/themes.ts @@ -2,7 +2,6 @@ import { catppuccin } from './themes/catppuccin'; import { github } from './themes/github'; import { hotdogStand } from './themes/hotdog-stand'; import { monokaiPro } from './themes/monokai-pro'; -import { relaxing } from './themes/relaxing'; import { rosePine } from './themes/rose-pine'; import { yaak, yaakDark, yaakLight } from './themes/yaak'; @@ -12,7 +11,6 @@ export const defaultLightTheme = yaakLight; export const yaakThemes = [ ...yaak, ...catppuccin, - ...relaxing, ...rosePine, ...github, ...monokaiPro, diff --git a/src-web/lib/theme/themes/github.ts b/src-web/lib/theme/themes/github.ts index d79c0940..59f30026 100644 --- a/src-web/lib/theme/themes/github.ts +++ b/src-web/lib/theme/themes/github.ts @@ -3,7 +3,7 @@ import type { YaakTheme } from '../window'; const githubDark: YaakTheme = { id: 'github-dark', - name: 'GitHub', + name: 'GitHub Dark', background: new Color('#0d1218', 'dark'), backgroundHighlight: new Color('#171c23', 'dark'), backgroundHighlightSecondary: new Color('#1c2127', 'dark'), @@ -36,7 +36,7 @@ const githubDark: YaakTheme = { export const githubLight: YaakTheme = { id: 'github-light', - name: 'GitHub', + name: 'GitHub Light', background: new Color('#ffffff', 'light'), backgroundHighlight: new Color('hsl(210,15%,92%)', 'light'), backgroundHighlightSecondary: new Color('hsl(210,29%,94%)', 'light'), diff --git a/src-web/lib/theme/themes/relaxing.ts b/src-web/lib/theme/themes/relaxing.ts deleted file mode 100644 index 76466b66..00000000 --- a/src-web/lib/theme/themes/relaxing.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Color } from '../color'; -import type { YaakTheme } from '../window'; - -const relaxingDefault: YaakTheme = { - name: 'Relaxing', - id: 'relaxing', - background: new Color('#2b1e3b', 'dark'), - foreground: new Color('#ede2f5', 'dark'), - colors: { - primary: new Color('#cba6f7', 'dark'), - secondary: new Color('#bac2de', 'dark'), - info: new Color('#89b4fa', 'dark'), - success: new Color('#a6e3a1', 'dark'), - notice: new Color('#f9e2af', 'dark'), - warning: new Color('#fab387', 'dark'), - danger: new Color('#f38ba8', 'dark'), - }, -}; - -export const relaxing = [relaxingDefault]; diff --git a/src-web/lib/theme/themes/yaak.ts b/src-web/lib/theme/themes/yaak.ts index a5e229fb..d38e0fad 100644 --- a/src-web/lib/theme/themes/yaak.ts +++ b/src-web/lib/theme/themes/yaak.ts @@ -3,7 +3,7 @@ import type { YaakTheme } from '../window'; export const yaakLight: YaakTheme = { id: 'yaak-light', - name: 'Yaak', + name: 'Yaak Light', background: new Color('hsl(216,24%,100%)', 'light'), backgroundHighlight: new Color('hsl(216,24%,93%)', 'light'), backgroundHighlightSecondary: new Color('hsl(216,24%,87%)', 'light'), @@ -30,7 +30,7 @@ export const yaakLight: YaakTheme = { export const yaakDark: YaakTheme = { id: 'yaak-dark', - name: 'Yaak', + name: 'Yaak Dark', background: new Color('hsl(244,23%,14%)', 'dark'), backgroundHighlight: new Color('hsl(244,23%,23%)', 'dark'), backgroundHighlightSecondary: new Color('hsl(244,23%,20%)', 'dark'),