tauri-apps / tauri

Build smaller, faster, and more secure desktop applications with a web frontend.
https://tauri.app
Apache License 2.0
78.53k stars 2.33k forks source link

[feat] Allowing for native inset OS X traffic lights on NSWindow #4789

Open haasal opened 1 year ago

haasal commented 1 year ago

Describe the problem

Many apps (also electron apps) have customized traffic light position to fit their style better. In electron this is achieved through a config file that calls native objc code. I would like this in tauri as it would allow for cleaner more intentional designs.

Describe the solution you'd like

I have already worked on this in issue #2663. I used the electron source as inspiration.

A file called window_ext.rs:

use tauri::{Runtime, Window};

pub trait WindowExt {
    #[cfg(target_os = "macos")]
    fn set_transparent_titlebar(&self, transparent: bool);
    fn position_traffic_lights(&self, x: f64, y: f64);
}

impl<R: Runtime> WindowExt for Window<R> {
    #[cfg(target_os = "macos")]
    fn set_transparent_titlebar(&self, transparent: bool) {
        use cocoa::appkit::{NSWindow, NSWindowTitleVisibility};

        let window = self.ns_window().unwrap() as cocoa::base::id;

        unsafe {
            window.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden);

            if transparent {
                window.setTitlebarAppearsTransparent_(cocoa::base::YES);
            } else {
                window.setTitlebarAppearsTransparent_(cocoa::base::NO);
            }
        }
    }

    #[cfg(target_os = "macos")]
    fn position_traffic_lights(&self, x: f64, y: f64) {
        use cocoa::appkit::{NSView, NSWindow, NSWindowButton};
        use cocoa::foundation::NSRect;

        let window = self.ns_window().unwrap() as cocoa::base::id;

        unsafe {
            let close = window.standardWindowButton_(NSWindowButton::NSWindowCloseButton);
            let miniaturize =
                window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton);
            let zoom = 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 + 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;
            let _: () = msg_send![title_bar_container_view, setFrame: title_bar_rect];

            let window_buttons = vec![close, miniaturize, zoom];
            let space_between = NSView::frame(miniaturize).origin.x - NSView::frame(close).origin.x;

            for (i, button) in window_buttons.into_iter().enumerate() {
                let mut rect: NSRect = NSView::frame(button);
                rect.origin.x = x + (i as f64 * space_between);
                button.setFrameOrigin(rect.origin);
            }
        }
    }
}

main.rs:

#![cfg_attr(
    all(not(debug_assertions), target_os = "windows"),
    windows_subsystem = "windows"
)]

#[cfg(target_os = "macos")]
#[macro_use]
extern crate objc;

use tauri::{Manager, WindowEvent};
use window_ext::WindowExt;

mod window_ext;

fn main() {
    tauri::Builder::default()
        .setup(|app| {
            let win = app.get_window("main").unwrap();
            win.set_transparent_titlebar(true);
            win.position_traffic_lights(30.0, 30.0);
            Ok(())
        })
        .on_window_event(|e| {
            if let WindowEvent::Resized(..) = e.event() {
                let win = e.window();
                win.position_traffic_lights(30., 30.);
            }
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");

}

In general this works:

Screenshot 2022-07-27 at 14 36 04

The on_window_event listener is there because without it the traffic lights would take their original position when the window is resized. This comes however with an annoying problem: The NSWindow handle becomes only available after the window is already rendered. That causes some annoying artifacts. You can see the original traffic lights before they are repositioned for a very short time.

So the task is somehow tapping into the tao render pipeline or something else to draw the traffic lights before the window is rendered.

Alternatives considered

I don't really have any. Maybe somehow controlling the NSWindow completely ourselves. Please make suggestions if you know more about the tauri source.

Additional context

There are still some #[cfg(target_os = "macos")] missing. Feel free to add them.

ghost commented 1 year ago

When changing the size of the window, there will be a problem of flashing OS X traffic lights, can this be solved?

JonasKruckenberg commented 1 year ago

So the task is somehow tapping into the tao render pipeline or something else to draw the traffic lights before the window is rendered.

I honestly don't believe that is possible, the traffic lights are part of the window and so everything there is rendered as one unit

JonasKruckenberg commented 1 year ago

Oh but in general I agree 100%, the current solution is definitely imperfect, but right now I rather have traffic lights that have ugly padding than traffic lights that flicker and jump around (this is not only a matter of taste, but also of usability)

ghost commented 1 year ago

After switching the system theme, the OSX traffic lights return to their original position

haasal commented 1 year ago

It’s definitely possible in native Tao: https://github.com/tauri-apps/tao/pull/513

this is a pull request I did on that issue. This works without artifacts. It’s not merged yet because of some conflicts. I have just now noticed that the PR wasn’t merged yet.

tr3ysmith commented 1 year ago

After switching the system theme, the OSX traffic lights return to their original position

@duhiqc you can adjust the above code to this, and that'll solve that issue:

.on_window_event(|e| {

    let apply_offset = || {
        let win = e.window();
        win.position_traffic_lights(30., 30.);
    }

    match e.event() {
        WindowEvent::Resized(..) => apply_offset(),
        WindowEvent::ThemeChanged(..) => apply_offset(),
        _ => {}
    }

})
KaliaJS commented 1 year ago

It's possible to just hide them ? For my app I don't need them.

schickling commented 1 year ago

I'm not a macOS developer and only did a few mins of research on this topic but I've found this gist. Maybe it's helpful in this context?

haasal commented 1 year ago

I don't really have the motivation, time or Know-how to work at this at the moment. Maybe someone else could take off from here? Please also take a look at my PR in Tao mentioned above somewhere. The Tao PR might get accepted into tauri.

haasal commented 1 year ago

It's possible to just hide them ? For my app I don't need them.

Yes. Definitely. I believe that's even officially possible through the tauri config file.

dukeeagle commented 1 year ago

Would love to see this go through! Subscribing to this thread.

raunakdoesdev commented 1 year ago

@dukeeagle funny to run into you on Tauri issues 😛

It's actually quite easy to implement this using the above two solutions. Feels native. Thanks for the great work @haasal and @tr3ysmith!

notbucai commented 8 months ago

切换系统主题后,OSX交通灯返回原来的位置

@duhiqc您可以将上面的代码调整为此,这将解决该问题:

.on_window_event(|e| {

    let apply_offset = || {
        let win = e.window();
        win.position_traffic_lights(30., 30.);
    }

    match e.event() {
        WindowEvent::Resized(..) => apply_offset(),
        WindowEvent::ThemeChanged(..) => apply_offset(),
        _ => {}
    }

})

After using this scheme, the taskbar will flicker.

https://github.com/tauri-apps/tauri/assets/32786133/2ed5195d-1e40-4fc8-8c1a-4b2e437e1f10

yooouuri commented 8 months ago

切换系统主题后,OSX交通灯返回原来的位置

@duhiqc您可以将上面的代码调整为此,这将解决该问题:

.on_window_event(|e| {

    let apply_offset = || {
        let win = e.window();
        win.position_traffic_lights(30., 30.);
    }

    match e.event() {
        WindowEvent::Resized(..) => apply_offset(),
        WindowEvent::ThemeChanged(..) => apply_offset(),
        _ => {}
    }

})

After using this scheme, the taskbar will flicker.

2023-09-22.10.36.51.mov

Same here

ilsur1103 commented 8 months ago

Couldn't fix the flickering issue, which was terribly annoying. But after some searching, I found this article: https://juejin.cn/post/7114740998579847198. In this article the author decided to go the way of using toolbar. I was quite satisfied with it. I hope it will help someone. image

haasal commented 8 months ago

I have already described this issue in my initial post. This is the reason why this issue was opened. Because currently there isn't really any way to get rid of this artifact. Please read the issue for more information.

As I have already mentioned there is a PR in Tao where I have worked on this but it has proven quite annoying and I currently don't have time to finish this.

haasal commented 7 months ago

Important Update: The PR for Tao was merged today. We now have to wait until a new Tao version is used by wry and then by tauri before the feature can be implemented in tauri itself.

appinteractive commented 7 months ago

Important Update: The PR for Tao was merged today. We now have to wait until a new Tao version is used by wry and then by tauri before the feature can be implemented in tauri itself.

Awesome! Will that be part of 1.x too?

PavelLaptev commented 6 months ago

awesome. Can't wait :-)

rickmartensnl commented 5 months ago

Any updates on this?

appinteractive commented 5 months ago

@haasal could you explain how to use this? I could not find any information on that whatsoever. Thanks

haasal commented 5 months ago

I forgot to mention this. There is a weird bug in wry which causes the new inset implementation in Tao not to work and I have no idea how to fix this. This has been a huge pain already which is really unfortunate. Help is appreciated! Wry Issue

@appinteractive it unfortunately doesn't work yet. The best you can do is setting the title bar style somewhere in the tauri config file

wyhaya commented 5 months ago

Hoppscotch implement this function and solve the flickering issue.

https://github.com/hoppscotch/hoppscotch/blob/286fcd2bb08a84f027b10308d1e18da368f95ebf/packages/hoppscotch-selfhost-desktop/src-tauri/src/mac/window.rs#L70-L101

appinteractive commented 4 months ago

Thanks @wyhaya that worked like a charm!

charrondev commented 2 months ago

I'm pretty new to Rust, but I followed along with https://github.com/hoppscotch/hoppscotch/blob/286fcd2bb08a84f027b10308d1e18da368f95ebf/packages/hoppscotch-selfhost-desktop/src-tauri/src/mac/window.rs#L70-L101 and updated it to work with Tauri 2.x

Gist here https://gist.github.com/charrondev/43150e940bd2771b1ea88256d491c7a9

In addition to working with Tauri 2.x it also works for all windows, rather than just the main one.