mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:08:16 -10:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fda6093b94 | ||
|
|
d0cdd589c7 |
No files matched your search
@@ -78,7 +78,7 @@ block2 = "0.6"
|
||||
# Plugin secrets in the login keychain.
|
||||
security-framework = "3.7"
|
||||
objc2 = "0.6"
|
||||
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSButton", "NSResponder", "NSView", "NSWindow"] }
|
||||
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSButton", "NSResponder", "NSScreen", "NSView", "NSWindow"] }
|
||||
objc2-core-foundation = { version = "0.3", default-features = false, features = ["std", "block2", "CFRunLoop"] }
|
||||
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "block2", "NSNotification", "NSOperation", "NSString"] }
|
||||
# Used only by the debug font-restriction override; Cargo dependency cfg cannot test debug_assertions.
|
||||
|
||||
@@ -124,9 +124,7 @@ pub fn window_cycle(app: AppHandle, window: WebviewWindow, forward: bool) {
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn window_toggle_fullscreen(window: WebviewWindow) -> IdeResult<bool> {
|
||||
let on = !window.is_fullscreen().map_err(|e| IdeError::internal(e.to_string()))?;
|
||||
window.set_fullscreen(on).map_err(|e| IdeError::internal(e.to_string()))?;
|
||||
Ok(on)
|
||||
crate::window_groups::toggle_fullscreen(&window)
|
||||
}
|
||||
|
||||
/// Height of the web title bar in native logical pixels (CSS height × IDE zoom).
|
||||
|
||||
+150
-1
@@ -2,7 +2,11 @@
|
||||
use crate::windows::{Launch, WindowsState};
|
||||
use ide_core::{IdeError, IdeResult};
|
||||
use ide_workspace::window_groups::{TabbingPreference, should_group_new_project};
|
||||
use objc2::{MainThreadMarker, rc::Retained, runtime::ProtocolObject};
|
||||
use objc2::{
|
||||
MainThreadMarker,
|
||||
rc::Retained,
|
||||
runtime::{AnyClass, AnyObject, Imp, ProtocolObject, Sel},
|
||||
};
|
||||
use objc2_app_kit::{NSWindow, NSWindowUserTabbingPreference};
|
||||
use objc2_foundation::{NSNotificationCenter, NSObjectProtocol};
|
||||
use tauri::{AppHandle, Manager, WebviewWindow};
|
||||
@@ -96,3 +100,148 @@ pub fn transfer_fullscreen(from: &WebviewWindow, to: &WebviewWindow) -> tauri::R
|
||||
native.toggleFullScreen(None);
|
||||
})
|
||||
}
|
||||
|
||||
/// Windows in simple fullscreen. Grouped project windows take turns in one Space; simple
|
||||
/// fullscreen covers the screen without a Space of its own, so switching projects is a plain
|
||||
/// show/hide instead of leaving one fullscreen Space and entering another.
|
||||
static SIMPLE_FULLSCREEN: std::sync::Mutex<Vec<String>> = std::sync::Mutex::new(Vec::new());
|
||||
|
||||
pub fn is_simple_fullscreen(label: &str) -> bool {
|
||||
SIMPLE_FULLSCREEN.lock().unwrap().iter().any(|l| l == label)
|
||||
}
|
||||
|
||||
pub fn set_simple_fullscreen(window: &WebviewWindow, on: bool) -> tauri::Result<()> {
|
||||
if !on {
|
||||
// Before the window goes back to its own frame.
|
||||
let target = window.clone();
|
||||
window.run_on_main_thread(move || keep_window_buttons(&target, false))?;
|
||||
}
|
||||
window.set_simple_fullscreen(on)?;
|
||||
let any = {
|
||||
let mut labels = SIMPLE_FULLSCREEN.lock().unwrap();
|
||||
labels.retain(|l| l != window.label());
|
||||
if on {
|
||||
labels.push(window.label().to_owned());
|
||||
}
|
||||
!labels.is_empty()
|
||||
};
|
||||
let target = window.clone();
|
||||
window.run_on_main_thread(move || {
|
||||
present(any);
|
||||
if on {
|
||||
keep_window_buttons(&target, true);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// Minimum sizes of windows in simple fullscreen, restored when they leave it.
|
||||
static MIN_SIZES: std::cell::RefCell<std::collections::HashMap<String, objc2_foundation::NSSize>> =
|
||||
std::cell::RefCell::default();
|
||||
}
|
||||
|
||||
/// Simple fullscreen drops the title bar and resizing; ours is transparent over the web toolbar,
|
||||
/// so keep it and its buttons, the green one (enabled only for a resizable window) leading back
|
||||
/// out of fullscreen. A minimum size of the whole screen keeps the edges from being dragged.
|
||||
fn keep_window_buttons(window: &WebviewWindow, on: bool) {
|
||||
use objc2_app_kit::NSWindowStyleMask;
|
||||
let Ok(pointer) = window.ns_window() else { return };
|
||||
let native = unsafe { &*pointer.cast::<NSWindow>() };
|
||||
let label = window.label().to_owned();
|
||||
if !on {
|
||||
if let Some(size) = MIN_SIZES.with_borrow_mut(|sizes| sizes.remove(&label)) {
|
||||
native.setMinSize(size);
|
||||
}
|
||||
return;
|
||||
}
|
||||
let Some(screen) = native.screen() else { return };
|
||||
let frame = screen.frame();
|
||||
MIN_SIZES.with_borrow_mut(|sizes| {
|
||||
sizes.entry(label).or_insert_with(|| native.minSize());
|
||||
});
|
||||
native.setMinSize(frame.size);
|
||||
native.setStyleMask(native.styleMask() | NSWindowStyleMask::Titled | NSWindowStyleMask::Resizable);
|
||||
native.setFrame_display(frame, true);
|
||||
}
|
||||
|
||||
/// Forgets a closed window; true when it was in simple fullscreen.
|
||||
pub fn forget_simple_fullscreen(app: &AppHandle, label: &str) -> bool {
|
||||
let (was, any) = {
|
||||
let mut labels = SIMPLE_FULLSCREEN.lock().unwrap();
|
||||
let before = labels.len();
|
||||
labels.retain(|l| l != label);
|
||||
(labels.len() != before, !labels.is_empty())
|
||||
};
|
||||
if was {
|
||||
let label = label.to_owned();
|
||||
let _ = app.run_on_main_thread(move || {
|
||||
MIN_SIZES.with_borrow_mut(|sizes| sizes.remove(&label));
|
||||
present(any);
|
||||
});
|
||||
}
|
||||
was
|
||||
}
|
||||
|
||||
/// Each window restores the presentation options it found on entering, which is wrong once
|
||||
/// fullscreen has passed between windows; derive them from the windows in fullscreen instead.
|
||||
fn present(fullscreen: bool) {
|
||||
use objc2_app_kit::{NSApplication, NSApplicationPresentationOptions};
|
||||
let Some(mtm) = MainThreadMarker::new() else { return };
|
||||
let options = if fullscreen {
|
||||
NSApplicationPresentationOptions::AutoHideDock | NSApplicationPresentationOptions::AutoHideMenuBar
|
||||
} else {
|
||||
NSApplicationPresentationOptions::Default
|
||||
};
|
||||
NSApplication::sharedApplication(mtm).setPresentationOptions(options);
|
||||
}
|
||||
|
||||
type ToggleFullScreen = unsafe extern "C-unwind" fn(*mut AnyObject, Sel, *mut AnyObject);
|
||||
|
||||
static NATIVE_TOGGLE_FULLSCREEN: std::sync::OnceLock<ToggleFullScreen> = std::sync::OnceLock::new();
|
||||
static APP: std::sync::OnceLock<AppHandle> = std::sync::OnceLock::new();
|
||||
|
||||
/// The green window button, the Globe-F shortcut and the system's Enter Full Screen item all
|
||||
/// send `toggleFullScreen:`; a grouped window answers it with simple fullscreen instead.
|
||||
pub fn intercept_toggle_fullscreen(window: &WebviewWindow) {
|
||||
let _ = APP.set(window.app_handle().clone());
|
||||
let _ = window.with_webview(|webview| unsafe {
|
||||
let class = objc2::ffi::object_getClass(webview.ns_window().cast()) as *mut AnyClass;
|
||||
let selector = objc2::sel!(toggleFullScreen:);
|
||||
if NATIVE_TOGGLE_FULLSCREEN.get().is_some() {
|
||||
return;
|
||||
}
|
||||
let Some(method) = (*class).instance_method(selector) else { return };
|
||||
let native: ToggleFullScreen = std::mem::transmute(method.implementation());
|
||||
let types = objc2::ffi::method_getTypeEncoding(method);
|
||||
let _ = NATIVE_TOGGLE_FULLSCREEN.set(native);
|
||||
let replacement: Imp = std::mem::transmute(toggle_fullscreen as ToggleFullScreen);
|
||||
objc2::ffi::class_replaceMethod(class, selector, replacement, types);
|
||||
});
|
||||
}
|
||||
|
||||
unsafe extern "C-unwind" fn toggle_fullscreen(this: *mut AnyObject, selector: Sel, sender: *mut AnyObject) {
|
||||
if let Some(window) = APP.get().and_then(|app| simple_fullscreen_window(app, this)) {
|
||||
if let Err(error) = crate::window_groups::toggle_fullscreen(&window) {
|
||||
tracing::warn!(message = error.message, "cannot toggle fullscreen");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if let Some(native) = NATIVE_TOGGLE_FULLSCREEN.get() {
|
||||
unsafe { native(this, selector, sender) };
|
||||
}
|
||||
}
|
||||
|
||||
/// `native` as a grouped project window (or one still in simple fullscreen) that is not in
|
||||
/// native fullscreen; one in native fullscreen leaves it the usual way.
|
||||
fn simple_fullscreen_window(app: &AppHandle, native: *mut AnyObject) -> Option<WebviewWindow> {
|
||||
let mask: objc2_app_kit::NSWindowStyleMask = unsafe { objc2::msg_send![native, styleMask] };
|
||||
if mask.contains(objc2_app_kit::NSWindowStyleMask::FullScreen) {
|
||||
return None;
|
||||
}
|
||||
let window = app
|
||||
.webview_windows()
|
||||
.into_values()
|
||||
.find(|w| w.ns_window().is_ok_and(|pointer| pointer.cast::<AnyObject>() == native))?;
|
||||
let grouped = app.state::<WindowsState>().groups.lock().group(window.label()).is_some();
|
||||
(grouped || is_simple_fullscreen(window.label())).then_some(window)
|
||||
}
|
||||
@@ -80,12 +80,61 @@ fn copy_frame(from: &WebviewWindow, to: &WebviewWindow) -> IdeResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn simple_fullscreen(label: &str) -> bool {
|
||||
#[cfg(target_os = "macos")]
|
||||
return crate::macos::is_simple_fullscreen(label);
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
let _ = label;
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn set_simple_fullscreen(window: &WebviewWindow, on: bool) -> IdeResult<()> {
|
||||
crate::macos::set_simple_fullscreen(window, on).map_err(|e| IdeError::internal(e.to_string()))
|
||||
}
|
||||
|
||||
/// Fullscreen for `window`. On macOS a grouped window uses simple fullscreen, for every member
|
||||
/// at once: the group stays in one Space and each member keeps the screen's size, so switching
|
||||
/// projects is only showing one window and hiding the others.
|
||||
pub fn toggle_fullscreen(window: &WebviewWindow) -> IdeResult<bool> {
|
||||
let err = |e: tauri::Error| IdeError::internal(e.to_string());
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let simple = simple_fullscreen(window.label());
|
||||
let app = window.app_handle();
|
||||
let members = app.state::<WindowsState>().groups.lock().group(window.label()).map(|g| g.members.clone());
|
||||
if simple || (members.is_some() && !window.is_fullscreen().map_err(err)?) {
|
||||
set_simple_fullscreen(window, !simple)?;
|
||||
for member in members.iter().flatten().filter(|m| *m != window.label()) {
|
||||
if let Some(other) = app.get_webview_window(member)
|
||||
&& simple_fullscreen(member) == simple
|
||||
{
|
||||
set_simple_fullscreen(&other, !simple)?;
|
||||
}
|
||||
}
|
||||
return Ok(!simple);
|
||||
}
|
||||
}
|
||||
let on = !window.is_fullscreen().map_err(err)?;
|
||||
window.set_fullscreen(on).map_err(err)?;
|
||||
Ok(on)
|
||||
}
|
||||
|
||||
pub fn select(app: &AppHandle, caller: &str, target: &str) -> IdeResult<()> {
|
||||
let to = app.get_webview_window(target).ok_or_else(|| IdeError::not_found("Window closed"))?;
|
||||
let state = app.state::<WindowsState>();
|
||||
let previous = state.groups.lock().select(caller, target)?;
|
||||
// In a fullscreen group the target already covers the screen, or does so before it is shown.
|
||||
let simple = previous.selected != target && simple_fullscreen(&previous.selected);
|
||||
let result = (|| {
|
||||
if previous.selected != target
|
||||
#[cfg(target_os = "macos")]
|
||||
if simple && !simple_fullscreen(target) {
|
||||
set_simple_fullscreen(&to, true)?;
|
||||
}
|
||||
if !simple
|
||||
&& previous.selected != target
|
||||
&& let Some(from) = app.get_webview_window(&previous.selected)
|
||||
{
|
||||
copy_frame(&from, &to)?;
|
||||
@@ -137,6 +186,10 @@ pub fn merge(app: &AppHandle, caller: &str) -> IdeResult<()> {
|
||||
// Native fullscreen puts each window in a separate Space; leave it before grouping.
|
||||
for label in &members {
|
||||
if let Some(window) = app.get_webview_window(label) {
|
||||
#[cfg(target_os = "macos")]
|
||||
if label != caller && simple_fullscreen(label) {
|
||||
set_simple_fullscreen(&window, false)?;
|
||||
}
|
||||
window.set_fullscreen(false).map_err(|e| IdeError::internal(e.to_string()))?;
|
||||
}
|
||||
}
|
||||
@@ -148,10 +201,20 @@ pub fn detach(app: &AppHandle, caller: &str) -> IdeResult<()> {
|
||||
let state = app.state::<WindowsState>();
|
||||
let next = state.groups.lock().detach(caller);
|
||||
if let Some(next) = next {
|
||||
// The tab that stays keeps the fullscreen; the detached window opens beside it.
|
||||
let simple = simple_fullscreen(caller);
|
||||
if let Some(window) = app.get_webview_window(&next) {
|
||||
#[cfg(target_os = "macos")]
|
||||
if simple && !simple_fullscreen(&next) {
|
||||
set_simple_fullscreen(&window, true)?;
|
||||
}
|
||||
window.show().map_err(|e| IdeError::internal(e.to_string()))?;
|
||||
}
|
||||
if let Some(window) = app.get_webview_window(caller) {
|
||||
#[cfg(target_os = "macos")]
|
||||
if simple {
|
||||
set_simple_fullscreen(&window, false)?;
|
||||
}
|
||||
if let Ok(position) = window.outer_position() {
|
||||
let _ = window.set_position(tauri::PhysicalPosition::new(position.x + 32, position.y + 32));
|
||||
}
|
||||
@@ -164,9 +227,15 @@ pub fn detach(app: &AppHandle, caller: &str) -> IdeResult<()> {
|
||||
|
||||
pub fn destroyed(app: &AppHandle, caller: &str) {
|
||||
let Some(state) = app.try_state::<WindowsState>() else { return };
|
||||
#[cfg(target_os = "macos")]
|
||||
let simple = crate::macos::forget_simple_fullscreen(app, caller);
|
||||
let next = state.groups.lock().detach(caller);
|
||||
if !state.quitting() {
|
||||
if let Some(window) = next.and_then(|label| app.get_webview_window(&label)) {
|
||||
#[cfg(target_os = "macos")]
|
||||
if simple && !simple_fullscreen(window.label()) {
|
||||
let _ = set_simple_fullscreen(&window, true);
|
||||
}
|
||||
windows::focus(&window);
|
||||
}
|
||||
let app = app.clone();
|
||||
|
||||
@@ -602,6 +602,8 @@ fn create_window_for(
|
||||
app.state::<WindowsState>().set_launch(&label, launch);
|
||||
let window = build_project_window(app, config)?;
|
||||
#[cfg(target_os = "macos")]
|
||||
crate::macos::intercept_toggle_fullscreen(&window);
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Some(target) = tab_target {
|
||||
crate::window_groups::join(app, target.label(), window.label())?;
|
||||
return Ok(window);
|
||||
|
||||
Reference in new issue
Block a user