From 4847aeb784762aee0150c93e27601bcd754e7b78 Mon Sep 17 00:00:00 2001 From: Iason Paraskevopoulos Date: Sat, 15 Nov 2025 22:44:58 +0000 Subject: [PATCH 1/3] feat: add maximise button to screenshare window --- tauri/src-tauri/src/main.rs | 39 +++++++ tauri/src/App.css | 10 +- .../src/components/SharingScreen/Controls.tsx | 10 +- .../SharingScreen/SharingScreen.tsx | 12 +- tauri/src/components/SharingScreen/utils.ts | 93 ++++++++++------ tauri/src/windows/screensharing/context.tsx | 5 + tauri/src/windows/screensharing/main.tsx | 104 +++++++++++++++++- tauri/src/windows/window-utils.ts | 13 +++ 8 files changed, 241 insertions(+), 45 deletions(-) diff --git a/tauri/src-tauri/src/main.rs b/tauri/src-tauri/src/main.rs index 76dc851..411bf59 100644 --- a/tauri/src-tauri/src/main.rs +++ b/tauri/src-tauri/src/main.rs @@ -499,6 +499,44 @@ fn get_livekit_url(app: tauri::AppHandle) -> String { data.livekit_server_url.clone() } +#[tauri::command] +fn style_screenshare_window(app: tauri::AppHandle) -> Result<(), String> { + let Some(window) = app.get_webview_window("screenshare") else { + return Err("screenshare window not found".to_string()); + }; + + #[cfg(target_os = "macos")] + { + use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial, NSVisualEffectState}; + + if let Err(e) = apply_vibrancy( + &window, + NSVisualEffectMaterial::HudWindow, + Some(NSVisualEffectState::Active), + Some(16.0), + ) { + log::warn!("Failed to apply vibrancy to screenshare window: {}", e); + } + + set_window_corner_radius(&window, 16.0); + } + + #[cfg(target_os = "windows")] + { + use window_vibrancy::apply_blur; + + if let Err(e) = apply_blur(&window, Some((18, 18, 18, 125))) { + log::warn!("Failed to apply blur to screenshare window: {}", e); + } + } + + if let Err(e) = window.set_shadow(true) { + log::warn!("Failed to set shadow for screenshare window: {}", e); + } + + Ok(()) +} + #[tauri::command] async fn create_camera_window(app: tauri::AppHandle, camera_token: String) -> Result<(), String> { log::info!("create_camera_window with token: {}", camera_token); @@ -934,6 +972,7 @@ fn main() { get_camera_permission, open_camera_settings, create_camera_window, + style_screenshare_window, set_sentry_metadata, call_started, ]) diff --git a/tauri/src/App.css b/tauri/src/App.css index 0ab5321..1414f07 100644 --- a/tauri/src/App.css +++ b/tauri/src/App.css @@ -407,8 +407,16 @@ body { } .screenshare-body { + margin: 0; + height: 100vh; + width: 100vw; overflow: hidden; - background-color: var(--color-slate-900); + background-color: transparent; +} + +.screenshare-body #root { + height: 100%; + width: 100%; } .screenshare-video-focus, diff --git a/tauri/src/components/SharingScreen/Controls.tsx b/tauri/src/components/SharingScreen/Controls.tsx index 42417f1..0981940 100644 --- a/tauri/src/components/SharingScreen/Controls.tsx +++ b/tauri/src/components/SharingScreen/Controls.tsx @@ -1,5 +1,4 @@ import { HiOutlineCursorClick } from "react-icons/hi"; -import { LiaHandPointerSolid } from "react-icons/lia"; import { useSharingContext } from "@/windows/screensharing/context"; import { TooltipContent, TooltipTrigger, Tooltip, TooltipProvider } from "../ui/tooltip"; import { BiSolidJoystick } from "react-icons/bi"; @@ -7,8 +6,13 @@ import useStore from "@/store/store"; import { SegmentedControl } from "../ui/segmented-control"; import { useState } from "react"; import { CustomIcons } from "../ui/icons"; +import { cn } from "@/lib/utils"; -export function ScreenSharingControls() { +type ScreenSharingControlsProps = { + className?: string; +}; + +export function ScreenSharingControls({ className }: ScreenSharingControlsProps = {}) { const { setIsSharingKeyEvents, setIsSharingMouse } = useSharingContext(); const isRemoteControlEnabled = useStore((state) => state.callTokens?.isRemoteControlEnabled); const [remoteControlStatus, setRemoteControlStatus] = useState("controlling"); @@ -26,7 +30,7 @@ export function ScreenSharingControls() { return ( -
+
{ onlySubscribed: true, }); const localParticipant = useLocalParticipant(); - let { isSharingMouse, isSharingKeyEvents, parentKeyTrap } = useSharingContext(); + const { isSharingMouse, isSharingKeyEvents, parentKeyTrap, setStreamDimensions } = useSharingContext(); const [wrapperRef, isMouseInside] = useHover(); const { updateCallTokens } = useStore(); const [mouse, mouseRef] = useMouse(); @@ -263,6 +263,14 @@ const ConsumerComponent = React.memo(() => { } }, [track, streamWidth, streamHeight]); + useEffect(() => { + if (track) { + setStreamDimensions({ width: streamWidth, height: streamHeight }); + } else { + setStreamDimensions(null); + } + }, [track, streamWidth, streamHeight, setStreamDimensions]); + /* * We do this because we need a way to retrigger the useEffect below, * adding the videoRef.current to the dependency array doesn't work because @@ -525,7 +533,7 @@ const ConsumerComponent = React.memo(() => { }, [isMouseInside, isSharingKeyEvents, parentKeyTrap]); const clearClipboard = useCallback(async () => { - await writeText(""); + await writeText(""); }, []); useEffect(() => { diff --git a/tauri/src/components/SharingScreen/utils.ts b/tauri/src/components/SharingScreen/utils.ts index 5f6dbaf..c0fef62 100644 --- a/tauri/src/components/SharingScreen/utils.ts +++ b/tauri/src/components/SharingScreen/utils.ts @@ -1,34 +1,19 @@ import { OS } from "@/constants"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; -import { getCurrentWindow, PhysicalSize, LogicalSize } from "@tauri-apps/api/window"; -import { currentMonitor } from "@tauri-apps/api/window"; +import { PhysicalSize, LogicalSize, currentMonitor } from "@tauri-apps/api/window"; const appWindow = getCurrentWebviewWindow(); -/* - * This function resizes the window to fit the stream's aspect ratio. - * - * It assumes that the stream's aspect ratio is greater than 1 (width > height). - * There are two possible scenarios that need to be handled in order to avoid - * the window to overflowing the screen (we could only overflow the heigth because - * we this is calculated from the width using the aspect ratio): - * - The monitor's width is greater than its height, and then - * we need to make sure the stream doesn't overflow the height. - * - * In this case we calculate the max width from the monitor's height - * and don't allow the window to have a width greater than the - * calculated one. - * - * - The monitor's height is greater than its width, and then - * we need to make sure the stream doesn't overflow the width. - * - * In this case we calculate the max height from the monitor's width - * and don't allow the window to have a height greater than the - * calculated one. - */ -export async function resizeWindow(streamWidth: number, streamHeight: number, ref: React.RefObject) { +type WindowSizingResult = { + maxContentWidth: number; + maxContentHeight: number; + aspectRatio: number; + streamExtraOffset: number; +}; + +const calculateWindowSizing = async (streamWidth: number, streamHeight: number): Promise => { if (streamWidth === 16 && streamHeight === 9) { - return; + return null; } const monitor = await currentMonitor(); const monitorWidth = monitor?.size.width || 0; @@ -63,15 +48,41 @@ export async function resizeWindow(streamWidth: number, streamHeight: number, re let maxHeight = Math.floor(monitorHeight - taskbarHeight - streamExtraOffset); let maxWidth = Math.floor(monitorWidth); - if (maxWidth > 0 && maxHeight > 0) { - if (monitorWidth >= monitorHeight) { - maxWidth = Math.floor(maxHeight * aspectRatio); - } else { - maxHeight = Math.floor(maxWidth / aspectRatio); - } - appWindow.setMaxSize(new LogicalSize(maxWidth, maxHeight + streamExtraOffset)); + if (maxWidth <= 0 || maxHeight <= 0) { + return null; } + if (monitorWidth >= monitorHeight) { + maxWidth = Math.floor(maxHeight * aspectRatio); + } else { + maxHeight = Math.floor(maxWidth / aspectRatio); + } + + return { + maxContentWidth: maxWidth, + maxContentHeight: maxHeight, + aspectRatio, + streamExtraOffset, + }; +}; + +/* + * This function resizes the window to fit the stream's aspect ratio. + * + * It assumes that the stream's aspect ratio is greater than 1 (width > height). + * There are two possible scenarios that need to be handled in order to avoid + * the window to overflowing the screen (we could only overflow the height because + * this is calculated from the width using the aspect ratio). + */ +export async function resizeWindow(streamWidth: number, streamHeight: number, ref: React.RefObject) { + const sizing = await calculateWindowSizing(streamWidth, streamHeight); + if (!sizing) { + return; + } + + const { maxContentWidth, maxContentHeight, aspectRatio, streamExtraOffset } = sizing; + appWindow.setMaxSize(new LogicalSize(maxContentWidth, maxContentHeight + streamExtraOffset)); + let size = await appWindow.innerSize(); if (ref.current) { @@ -79,8 +90,8 @@ export async function resizeWindow(streamWidth: number, streamHeight: number, re return; } - const newWidth = Math.min(size.width, maxWidth); - const newHeight = Math.min(Math.floor(size.width / aspectRatio), maxHeight) + streamExtraOffset; + const newWidth = Math.min(size.width, maxContentWidth); + const newHeight = Math.min(Math.floor(size.width / aspectRatio), maxContentHeight) + streamExtraOffset; console.log(`Current size is ${size.width}x${size.height}; New size will be ${newWidth}x${newHeight}`); appWindow.setSize( // As the video will be always full window width minus some padding, @@ -89,3 +100,17 @@ export async function resizeWindow(streamWidth: number, streamHeight: number, re ); } } + +export async function setWindowToMaxStreamSize(streamWidth: number, streamHeight: number) { + const sizing = await calculateWindowSizing(streamWidth, streamHeight); + if (!sizing) { + return; + } + + const { maxContentWidth, maxContentHeight, streamExtraOffset } = sizing; + + await appWindow.setMaxSize(new LogicalSize(maxContentWidth, maxContentHeight + streamExtraOffset)); + await appWindow.setSize( + new PhysicalSize(Math.floor(maxContentWidth), Math.floor(maxContentHeight + streamExtraOffset)), + ); +} diff --git a/tauri/src/windows/screensharing/context.tsx b/tauri/src/windows/screensharing/context.tsx index 4ba68cd..8a9772e 100644 --- a/tauri/src/windows/screensharing/context.tsx +++ b/tauri/src/windows/screensharing/context.tsx @@ -9,6 +9,8 @@ type SharingContextType = { setVideoToken: (value: string) => void; parentKeyTrap?: HTMLDivElement; setParentKeyTrap: (value: HTMLDivElement) => void; + streamDimensions: { width: number; height: number } | null; + setStreamDimensions: (value: { width: number; height: number } | null) => void; }; const SharingContext = createContext(undefined); @@ -30,6 +32,7 @@ export const SharingProvider: React.FC = ({ children }) => const [isSharingKeyEvents, setIsSharingKeyEvents] = useState(true); const [parentKeyTrap, setParentKeyTrap] = useState(undefined); const [videoToken, setVideoToken] = useState(null); + const [streamDimensions, setStreamDimensions] = useState<{ width: number; height: number } | null>(null); return ( = ({ children }) => setParentKeyTrap, videoToken, setVideoToken, + streamDimensions, + setStreamDimensions, }} > {children} diff --git a/tauri/src/windows/screensharing/main.tsx b/tauri/src/windows/screensharing/main.tsx index b079f61..d868b92 100644 --- a/tauri/src/windows/screensharing/main.tsx +++ b/tauri/src/windows/screensharing/main.tsx @@ -1,14 +1,18 @@ import "@/services/sentry"; import "../../App.css"; -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import ReactDOM from "react-dom/client"; import { SharingScreen } from "@/components/SharingScreen/SharingScreen"; import { SharingProvider, useSharingContext } from "./context"; import { ScreenSharingControls } from "@/components/SharingScreen/Controls"; import { Toaster } from "react-hot-toast"; import { useDisableNativeContextMenu } from "@/lib/hooks"; +import { cn } from "@/lib/utils"; import { tauriUtils } from "../window-utils"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; +import { PhysicalSize } from "@tauri-apps/api/window"; +import { setWindowToMaxStreamSize } from "@/components/SharingScreen/utils"; +import { LuMaximize2, LuMinus, LuX } from "react-icons/lu"; const appWindow = getCurrentWebviewWindow(); @@ -20,10 +24,43 @@ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( , ); +const TitlebarButton = ({ + onClick, + disabled, + children, + label, +}: { + onClick?: () => void; + disabled?: boolean; + children: React.ReactNode; + label: string; +}) => { + return ( + + ); +}; + function Window() { useDisableNativeContextMenu(); - const { setParentKeyTrap, setVideoToken, videoToken } = useSharingContext(); + const { setParentKeyTrap, setVideoToken, videoToken, streamDimensions } = useSharingContext(); const [livekitUrl, setLivekitUrl] = useState(""); + const previousSizeRef = useRef<{ width: number; height: number } | null>(null); + const [isMaximized, setIsMaximized] = useState(false); useEffect(() => { const videoTokenFromUrl = tauriUtils.getTokenParam("videoToken"); @@ -43,17 +80,74 @@ function Window() { } enableDock(); + + tauriUtils.styleScreenshareWindow(); + }, []); + + const handleClose = useCallback(() => { + appWindow.close(); }, []); + const handleMinimize = useCallback(async () => { + const minimized = await appWindow.isMinimized(); + if (minimized) { + await appWindow.show(); + await appWindow.unminimize(); + await appWindow.setFocus(); + } else { + await appWindow.minimize(); + } + }, []); + + const handleFullscreen = useCallback(async () => { + if (!streamDimensions) { + return; + } + + if (!isMaximized) { + const size = await appWindow.innerSize(); + previousSizeRef.current = { width: size.width, height: size.height }; + await setWindowToMaxStreamSize(streamDimensions.width, streamDimensions.height); + setIsMaximized(true); + } else if (previousSizeRef.current) { + await appWindow.setSize(new PhysicalSize(previousSizeRef.current.width, previousSizeRef.current.height)); + previousSizeRef.current = null; + setIsMaximized(false); + } + }, [streamDimensions, isMaximized]); + + const fullscreenDisabled = !streamDimensions; + return (
ref && setParentKeyTrap(ref)} > -
- +
+
+ + + + + + + + + +
+
+ +
+
{videoToken && } diff --git a/tauri/src/windows/window-utils.ts b/tauri/src/windows/window-utils.ts index faf6968..fe3c206 100644 --- a/tauri/src/windows/window-utils.ts +++ b/tauri/src/windows/window-utils.ts @@ -26,6 +26,9 @@ const createScreenShareWindow = async (videoToken: string, bringToFront: boolean height: 450, url: URL, hiddenTitle: true, + decorations: false, + transparent: true, + shadow: true, titleBarStyle: "overlay", resizable: true, // alwaysOnTop: true, @@ -233,6 +236,15 @@ const setDockIconVisible = async (visible: boolean) => { await invoke("set_dock_icon_visible", { visible }); }; +const styleScreenshareWindow = async () => { + if (!isTauri) return; + try { + await invoke("style_screenshare_window"); + } catch (error) { + console.error("Failed to style screenshare window:", error); + } +}; + const getLastUsedMic = async () => { return await invoke("get_last_used_mic"); }; @@ -288,6 +300,7 @@ export const tauriUtils = { getScreenSharePermission, getCameraPermission, setDockIconVisible, + styleScreenshareWindow, getLastUsedMic, setLastUsedMic, minimizeMainWindow, From c203fdb5ff982a0c5a80af5d56ad8e6a6e51d849 Mon Sep 17 00:00:00 2001 From: Iason Paraskevopoulos Date: Sun, 16 Nov 2025 10:27:00 +0000 Subject: [PATCH 2/3] chore: minor adjustments --- tauri/src-tauri/src/main.rs | 103 ++++++++++++++++------- tauri/src/windows/screensharing/main.tsx | 2 - tauri/src/windows/window-utils.ts | 41 +++------ 3 files changed, 83 insertions(+), 63 deletions(-) diff --git a/tauri/src-tauri/src/main.rs b/tauri/src-tauri/src/main.rs index 411bf59..0a6b13b 100644 --- a/tauri/src-tauri/src/main.rs +++ b/tauri/src-tauri/src/main.rs @@ -500,39 +500,80 @@ fn get_livekit_url(app: tauri::AppHandle) -> String { } #[tauri::command] -fn style_screenshare_window(app: tauri::AppHandle) -> Result<(), String> { - let Some(window) = app.get_webview_window("screenshare") else { - return Err("screenshare window not found".to_string()); - }; +async fn create_screenshare_window( + app: tauri::AppHandle, + video_token: String, +) -> Result<(), String> { + if let Some(window) = app.get_webview_window("screenshare") { + let _ = window.show(); + let _ = window.set_focus(); + return Ok(()); + } + + let url = format!("screenshare.html?videoToken={}", video_token); + + #[allow(unused_mut)] + let mut window_builder = + WebviewWindowBuilder::new(&app, "screenshare", WebviewUrl::App(url.into())) + .title("Screen sharing") + .inner_size(800.0, 450.0) + .resizable(true) + .visible(false) + .transparent(true) + .decorations(false) + .shadow(true) + .always_on_top(false) + .maximizable(false); #[cfg(target_os = "macos")] { - use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial, NSVisualEffectState}; - - if let Err(e) = apply_vibrancy( - &window, - NSVisualEffectMaterial::HudWindow, - Some(NSVisualEffectState::Active), - Some(16.0), - ) { - log::warn!("Failed to apply vibrancy to screenshare window: {}", e); - } - - set_window_corner_radius(&window, 16.0); + window_builder = window_builder.hidden_title(true) } - #[cfg(target_os = "windows")] - { - use window_vibrancy::apply_blur; + let screenshare_window = window_builder + .build() + .map_err(|e| format!("Failed to create screenshare window: {}", e))?; - if let Err(e) = apply_blur(&window, Some((18, 18, 18, 125))) { - log::warn!("Failed to apply blur to screenshare window: {}", e); - } - } + let window_clone = screenshare_window.clone(); - if let Err(e) = window.set_shadow(true) { - log::warn!("Failed to set shadow for screenshare window: {}", e); - } + screenshare_window + .run_on_main_thread(move || { + #[cfg(target_os = "macos")] + { + use window_vibrancy::{ + apply_vibrancy, NSVisualEffectMaterial, NSVisualEffectState, + }; + + if let Err(e) = apply_vibrancy( + &window_clone, + NSVisualEffectMaterial::HudWindow, + Some(NSVisualEffectState::Active), + Some(16.0), + ) { + log::warn!("Failed to apply vibrancy to screenshare window: {}", e); + } + + set_window_corner_radius(&window_clone, 16.0); + } + + #[cfg(target_os = "windows")] + { + use window_vibrancy::apply_blur; + + if let Err(e) = apply_blur(&window_clone, Some((18, 18, 18, 125))) { + log::warn!("Failed to apply blur to screenshare window: {}", e); + } + } + + if let Err(e) = window_clone.show() { + log::error!("Failed to show screenshare window: {}", e); + } + + if let Err(e) = window_clone.set_focus() { + log::error!("Failed to focus screenshare window: {}", e); + } + }) + .map_err(|e| format!("Failed to run on main thread: {}", e))?; Ok(()) } @@ -557,9 +598,7 @@ async fn create_camera_window(app: tauri::AppHandle, camera_token: String) -> Re #[cfg(target_os = "macos")] { - window_builder = window_builder - .hidden_title(true) - .title_bar_style(tauri::TitleBarStyle::Overlay); + window_builder = window_builder.hidden_title(true) } let camera_window = window_builder @@ -600,6 +639,10 @@ async fn create_camera_window(app: tauri::AppHandle, camera_token: String) -> Re if let Err(e) = window_clone.show() { log::error!("Failed to show camera window: {}", e); } + + if let Err(e) = window_clone.set_focus() { + log::error!("Failed to focus camera window: {}", e); + } }) .map_err(|e| format!("Failed to run on main thread: {}", e))?; @@ -972,7 +1015,7 @@ fn main() { get_camera_permission, open_camera_settings, create_camera_window, - style_screenshare_window, + create_screenshare_window, set_sentry_metadata, call_started, ]) diff --git a/tauri/src/windows/screensharing/main.tsx b/tauri/src/windows/screensharing/main.tsx index d868b92..d0e43e5 100644 --- a/tauri/src/windows/screensharing/main.tsx +++ b/tauri/src/windows/screensharing/main.tsx @@ -80,8 +80,6 @@ function Window() { } enableDock(); - - tauriUtils.styleScreenshareWindow(); }, []); const handleClose = useCallback(() => { diff --git a/tauri/src/windows/window-utils.ts b/tauri/src/windows/window-utils.ts index fe3c206..a812162 100644 --- a/tauri/src/windows/window-utils.ts +++ b/tauri/src/windows/window-utils.ts @@ -10,8 +10,6 @@ getVersion().then((version) => { }); const createScreenShareWindow = async (videoToken: string, bringToFront: boolean = true) => { - const URL = `screenshare.html?videoToken=${videoToken}`; - // Check if there is already a window open, // then focus on it and bring it to the front const isWindowOpen = await WebviewWindow.getByLabel("screenshare"); @@ -21,26 +19,17 @@ const createScreenShareWindow = async (videoToken: string, bringToFront: boolean } if (isTauri) { - const newWindow = new WebviewWindow("screenshare", { - width: 800, - height: 450, - url: URL, - hiddenTitle: true, - decorations: false, - transparent: true, - shadow: true, - titleBarStyle: "overlay", - resizable: true, - // alwaysOnTop: true, - maximizable: false, - alwaysOnTop: false, - visible: true, - title: "Screen sharing", - }); - newWindow.once("tauri://window-created", () => { - newWindow.setFocus(); - }); + try { + await invoke("create_screenshare_window", { videoToken }); + const windowHandle = await WebviewWindow.getByLabel("screenshare"); + if (windowHandle) { + await windowHandle.setFocus(); + } + } catch (error) { + console.error("Failed to create screenshare window:", error); + } } else { + const URL = `screenshare.html?videoToken=${videoToken}`; window.open(URL); } }; @@ -236,15 +225,6 @@ const setDockIconVisible = async (visible: boolean) => { await invoke("set_dock_icon_visible", { visible }); }; -const styleScreenshareWindow = async () => { - if (!isTauri) return; - try { - await invoke("style_screenshare_window"); - } catch (error) { - console.error("Failed to style screenshare window:", error); - } -}; - const getLastUsedMic = async () => { return await invoke("get_last_used_mic"); }; @@ -300,7 +280,6 @@ export const tauriUtils = { getScreenSharePermission, getCameraPermission, setDockIconVisible, - styleScreenshareWindow, getLastUsedMic, setLastUsedMic, minimizeMainWindow, From 782efabc6a111e04f3c0376e6deb3805d0200e2f Mon Sep 17 00:00:00 2001 From: konsalex Date: Tue, 18 Nov 2025 22:23:59 +0100 Subject: [PATCH 3/3] chore: styling and resize behaviour --- tauri/src/App.css | 2 +- .../SharingScreen/SharingScreen.tsx | 11 +- tauri/src/windows/screensharing/main.tsx | 119 ++++++++++++++---- 3 files changed, 106 insertions(+), 26 deletions(-) diff --git a/tauri/src/App.css b/tauri/src/App.css index 1414f07..942821a 100644 --- a/tauri/src/App.css +++ b/tauri/src/App.css @@ -421,7 +421,7 @@ body { .screenshare-video-focus, .screenshare-video:focus { - outline: 3px solid var(--color-yellow-300); + outline: 2px solid var(--color-yellow-300); outline-offset: -2px; } diff --git a/tauri/src/components/SharingScreen/SharingScreen.tsx b/tauri/src/components/SharingScreen/SharingScreen.tsx index 866a67b..ad37fde 100644 --- a/tauri/src/components/SharingScreen/SharingScreen.tsx +++ b/tauri/src/components/SharingScreen/SharingScreen.tsx @@ -636,10 +636,13 @@ const ConsumerComponent = React.memo(() => { return (
{DEBUGGING_VIDEO_TRACK && ( diff --git a/tauri/src/windows/screensharing/main.tsx b/tauri/src/windows/screensharing/main.tsx index d0e43e5..77263f0 100644 --- a/tauri/src/windows/screensharing/main.tsx +++ b/tauri/src/windows/screensharing/main.tsx @@ -6,13 +6,13 @@ import { SharingScreen } from "@/components/SharingScreen/SharingScreen"; import { SharingProvider, useSharingContext } from "./context"; import { ScreenSharingControls } from "@/components/SharingScreen/Controls"; import { Toaster } from "react-hot-toast"; -import { useDisableNativeContextMenu } from "@/lib/hooks"; +import { useDisableNativeContextMenu, useResizeListener } from "@/lib/hooks"; import { cn } from "@/lib/utils"; import { tauriUtils } from "../window-utils"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; -import { PhysicalSize } from "@tauri-apps/api/window"; +import { PhysicalSize, PhysicalPosition, currentMonitor } from "@tauri-apps/api/window"; import { setWindowToMaxStreamSize } from "@/components/SharingScreen/utils"; -import { LuMaximize2, LuMinus, LuX } from "react-icons/lu"; +import { LuMaximize2, LuMinimize2, LuMinus, LuX } from "react-icons/lu"; const appWindow = getCurrentWebviewWindow(); @@ -29,11 +29,13 @@ const TitlebarButton = ({ disabled, children, label, + className, }: { onClick?: () => void; disabled?: boolean; children: React.ReactNode; label: string; + className?: string; }) => { return ( ); }; @@ -59,8 +59,9 @@ function Window() { useDisableNativeContextMenu(); const { setParentKeyTrap, setVideoToken, videoToken, streamDimensions } = useSharingContext(); const [livekitUrl, setLivekitUrl] = useState(""); - const previousSizeRef = useRef<{ width: number; height: number } | null>(null); + const previousSizeRef = useRef<{ width: number; height: number; x: number; y: number } | null>(null); const [isMaximized, setIsMaximized] = useState(false); + const isProgrammaticResizeRef = useRef(false); useEffect(() => { const videoTokenFromUrl = tauriUtils.getTokenParam("videoToken"); @@ -82,6 +83,22 @@ function Window() { enableDock(); }, []); + // Detect manual window resizing and reset isMaximized state + const handleWindowResize = useCallback(() => { + // Ignore resize events during programmatic resizing + if (isProgrammaticResizeRef.current) { + return; + } + + // If window is marked as maximized but user manually resized, reset the state + if (isMaximized) { + setIsMaximized(false); + previousSizeRef.current = null; + } + }, [isMaximized]); + + useResizeListener(handleWindowResize); + const handleClose = useCallback(() => { appWindow.close(); }, []); @@ -103,14 +120,71 @@ function Window() { } if (!isMaximized) { + isProgrammaticResizeRef.current = true; const size = await appWindow.innerSize(); - previousSizeRef.current = { width: size.width, height: size.height }; - await setWindowToMaxStreamSize(streamDimensions.width, streamDimensions.height); + const position = await appWindow.innerPosition(); + previousSizeRef.current = { + width: size.width, + height: size.height, + x: position.x, + y: position.y, + }; + + // Get monitor info for centering + const monitor = await currentMonitor(); + if (!monitor) { + await setWindowToMaxStreamSize(streamDimensions.width, streamDimensions.height); + setIsMaximized(true); + isProgrammaticResizeRef.current = false; + return; + } + + // Calculate window size using 92% of screen height + const factor = await appWindow.scaleFactor(); + const streamExtraOffset = 50 * factor; + const aspectRatio = streamDimensions.width / streamDimensions.height; + + // Use 92% of monitor height + const maxHeight = Math.floor(monitor.size.height * 0.87); + const maxWidth = Math.floor(monitor.size.width); + + // Calculate width based on aspect ratio, ensuring it fits within screen bounds + let finalWidth: number; + let finalHeight: number; + + if (maxHeight * aspectRatio <= maxWidth) { + // Height is the limiting factor + finalHeight = Math.floor(maxHeight + streamExtraOffset); + finalWidth = Math.floor(maxHeight * aspectRatio); + } else { + // Width is the limiting factor + finalWidth = maxWidth; + finalHeight = Math.floor(maxWidth / aspectRatio + streamExtraOffset); + } + + // Set window size + await appWindow.setSize(new PhysicalSize(finalWidth, finalHeight)); + + // Center the window on the monitor + const centerX = Math.floor((monitor.size.width - finalWidth) / 2) + monitor.position.x; + const centerY = Math.floor((monitor.size.height - finalHeight) / 2) + monitor.position.y; + await appWindow.setPosition(new PhysicalPosition(centerX, centerY)); + setIsMaximized(true); + // Reset flag after a short delay to allow resize event to fire + setTimeout(() => { + isProgrammaticResizeRef.current = false; + }, 100); } else if (previousSizeRef.current) { + isProgrammaticResizeRef.current = true; await appWindow.setSize(new PhysicalSize(previousSizeRef.current.width, previousSizeRef.current.height)); + await appWindow.setPosition(new PhysicalPosition(previousSizeRef.current.x, previousSizeRef.current.y)); previousSizeRef.current = null; setIsMaximized(false); + // Reset flag after a short delay to allow resize event to fire + setTimeout(() => { + isProgrammaticResizeRef.current = false; + }, 100); } }, [streamDimensions, isMaximized]); @@ -118,28 +192,31 @@ function Window() { return (
ref && setParentKeyTrap(ref)} >
-
- - +
+ + - - + + - + {isMaximized ? + + : }