tauri-apps/tauri

Feature request: Add setting for titlebar style with native window controls support

fnky opened this issue Β· 54 comments

fnky commented

Is your feature request related to a problem? Please describe.

When creating frameless windows you're required to provide your own UI for window controls. This has multiple problems with consistency and accessibility and requires more work to get close to the UX that is already provided by the native window system.

For example on macOS there are different actions based on how you interact with the window controls ("traffic lights"):

  • Unfocused windows makes traffic lights appear gray
    • Unfocused windows still respond to mouse events for the traffic lights such as hovering.
  • Hovering the traffic lights will display the icons, based on system preference.
  • Holding Alt/Option key while clicking on "resize" will maximize the window as opposed to the default fullscreen behavior.
  • Traffic lights can still be used even if the underlying application becomes unresponsive.

And many more edge-cases that needs to be handled in order to provide native-like experience.

These cases only describes macOS-specific parts, but could easily apply to Windows and Linux as well, which would require more work to provide native-like UI.

Frameless windows also has several problems as outlined in #2549 and described in Electron's documentation

Describe the solution you'd like

Support the ability to set the titlebar style similarly to Electron's titleBarStyle setting which allows for custom title bars while preserving native window controls in different ways.

Describe alternatives you've considered

I don't believe there are any alternatives and needs to be solved within Tauri.

chhpt commented

Any progress?

gardc commented

Would love to see progress on this too, it's an important feature for me.

@chhpt @gardc we're still mainly blocked by the feature freeze here. The audit process is coming to an end and with the 1.0 release being so close, we're completely focused on that one.
To make our lives easier when merging the audit changes, we also closed the next branch for new features some time ago, fwiw.

Btw, if someone wants to help us out and take a stab at implementing this feature, we always appreciate PRs! ❀️

I will tackle this one. On my roadmap this week.

fnky commented

@FabianLars Thank you for the update, it's much appreciated. Can't wait to see whats in store for 1.0!

@JonasKruckenberg Great to hear! Let me know if there's anything I can do to assist with regards to macOS side of things.

awkj commented

I will tackle this one. On my roadmap this week.

Hello, The feature will come in tauri 2.0 or use tauri-plugin to complete it?

Alright, here goes my issue-only knowledge dump πŸ˜‚:

I'm struggling a bit with implementing this in tauri+tao directly, but here is how you can implement this yourself using the ns_window() handle and the cocoa crate:

use cocoa::appkit::{NSWindow, NSWindowStyleMask};
use tauri::{Runtime, Window};

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

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

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

      let mut style_mask = id.styleMask();
      style_mask.set(
        NSWindowStyleMask::NSFullSizeContentViewWindowMask,
        transparent,
      );
      id.setStyleMask_(style_mask);

      id.setTitleVisibility_(if transparent {
        NSWindowTitleVisibility::NSWindowTitleHidden
      } else {
        NSWindowTitleVisibility::NSWindowTitleVisible
      });
      id.setTitlebarAppearsTransparent_(if transparent {
        cocoa::base::YES
      } else {
        cocoa::base::NO
      });
    }
  }
}

This is how you would use the defined function in your app to make a window have a transparent titlebar.

fn main() {
   tauri::Builder::default()
      .setup(|app| {
         let win = app.get_window("main").unwrap();
         win.set_transparent_titlebar(true);

         Ok(())
      })
      .run(tauri::generate_context!())
      .expect("error while running tauri application");
}
awkj commented

Alright, here goes my issue-only knowledge dump πŸ˜‚:

I'm struggling a bit with implementing this in tauri+tao directly, but here is how you can implement this yourself using the ns_window() handle and the cocoa crate:

use cocoa::appkit::{NSWindow, NSWindowStyleMask};
use tauri::{Runtime, Window};

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

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

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

      let mut style_mask = id.styleMask();
      style_mask.set(
        NSWindowStyleMask::NSFullSizeContentViewWindowMask,
        transparent,
      );
      id.setStyleMask_(style_mask);

      id.setTitleVisibility_(if transparent {
        NSWindowTitleVisibility::NSWindowTitleHidden
      } else {
        NSWindowTitleVisibility::NSWindowTitleVisible
      });
      id.setTitlebarAppearsTransparent_(if transparent {
        cocoa::base::YES
      } else {
        cocoa::base::NO
      });
    }
  }
}

This is how you would use the defined function in your app to make a window have a transparent titlebar.

fn main() {
   tauri::Builder::default()
      .setup(|app| {
         let win = app.get_window("main").unwrap();
         win.set_transparent_titlebar(true);

         Ok(())
      })
      .run(tauri::generate_context!())
      .expect("error while running tauri application");
}

Thanks, It's work for me
image

but I not found API to hide toolbar(close maxsize fullsize button ) , if can hide the button, it will is perfect !

but I not found API to hide toolbar(close maxsize fullsize button ) , if can hide the button, it will is perfect !

This can be archived with the same NSWindowStyleMask that I used in the above example if you merely want to "grey-out" the minimize and close buttons.

If you're looking to hide all window buttons you should have chosen WindowBuilder::decorations(false) that has already been available for a while

awkj commented

WindowBuilder::decorations(false)

yes, I have use the API, it equal to setting tauri.conf.json "decoration" = false, but the setting will let window border lose round

yes, I have use the API, it equal to setting tauri.conf.json "decoration" = false, but the setting will let window border lose round

Do you want to publish your app to the macOS App Store? If not you can use "transparent": true and use css to recreate the border radius. It's as easy as that, no need for unsafe objective C interop.

awkj commented

yes, I have use the API, it equal to setting tauri.conf.json "decoration" = false, but the setting will let window border lose round

Do you want to publish your app to the macOS App Store? If not you can use "transparent": true and use css to recreate the border radius. It's as easy as that, no need for unsafe objective C interop.

the solution I found it in tauri document (https://tauri.studio/docs/guides/window-customization) ,but not perfect ,when I drag my windows , the border have display blank block on border. I want a setting can hide button and have round border(the style like native swift ui on mac)

Yes, I want publish to App Store , Why rust need use private API, but use swift or electron can ?

awkj commented

hello, I found the mask will let mouse can't move the window, it is indeterminacy for me is Bug or macos feauter, Can you give me a suggestion ?

 NSWindow::setTitlebarAppearsTransparent_(id, cocoa::base::YES);
            let mut style_mask = id.styleMask();
            style_mask.set(
                NSWindowStyleMask::NSFullSizeContentViewWindowMask,
                transparent,
            );
            id.setStyleMask_(style_mask);

I try call the possiable API but not work

id.setMovable_(cocoa::base::YES);
id.setMovableByWindowBackground_(cocoa::base::YES);

I use create a empty div and use data-tauri-drag-region to solve it ,but have some complex

<body>
  <div data-tauri-drag-region style="height:30px">
  </div>
  <div id="root"></div>
  <script type="module" src="/src/main.tsx"></script>
</body>

I use create a empty div and use data-tauri-drag-region to solve it ,but have some complex

<body>

  <div data-tauri-drag-region style="height:30px">

  </div>

  <div id="root"></div>

  <script type="module" src="/src/main.tsx"></script>

</body>

You need the data-tauri-drag-region to make it work as there is technically no titlebar anymore. You also need to enable window > startDragging, window > maximize and window > unmaximize in your allowlist

awkj commented

I use create a empty div and use data-tauri-drag-region to solve it ,but have some complex

<body>

  <div data-tauri-drag-region style="height:30px">

  </div>

  <div id="root"></div>

  <script type="module" src="/src/main.tsx"></script>

</body>

You need the data-tauri-drag-region to make it work as there is technically no titlebar anymore. You also need to enable window > startDragging, window > maximize and window > unmaximize in your allowlist

ok, I now setting tauri->allowlist is "all" and don't need the setting up again ,but this is good suggestion, this the doc https://tauri.studio/docs/api/config#tauri.allowlist.window wish help other user.

Great!
BTW, how to apply this feature on Windows platform? @JonasKruckenberg

Alright, here goes my issue-only knowledge dump πŸ˜‚:

I'm struggling a bit with implementing this in tauri+tao directly, but here is how you can implement this yourself using the ns_window() handle and the cocoa crate:

use cocoa::appkit::{NSWindow, NSWindowStyleMask};
use tauri::{Runtime, Window};

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

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

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

      let mut style_mask = id.styleMask();
      style_mask.set(
        NSWindowStyleMask::NSFullSizeContentViewWindowMask,
        transparent,
      );
      id.setStyleMask_(style_mask);

      id.setTitleVisibility_(if transparent {
        NSWindowTitleVisibility::NSWindowTitleHidden
      } else {
        NSWindowTitleVisibility::NSWindowTitleVisible
      });
      id.setTitlebarAppearsTransparent_(if transparent {
        cocoa::base::YES
      } else {
        cocoa::base::NO
      });
    }
  }
}

This is how you would use the defined function in your app to make a window have a transparent titlebar.

fn main() {
   tauri::Builder::default()
      .setup(|app| {
         let win = app.get_window("main").unwrap();
         win.set_transparent_titlebar(true);

         Ok(())
      })
      .run(tauri::generate_context!())
      .expect("error while running tauri application");
}

BTW, how to apply this feature on Windows platform? @JonasKruckenberg

This is not a thing on windows as far as I'm aware. The common practice (with electron too) seems to be to remove decorations entirely and create the titlebar from scratch using html/css/js

Got it. Thank you!

On Windows, removing decorations entirely and creating the titlebar from scratch using html/css/js, traffic lights can still be used even if the underlying application becomes inactive.

But on macOS, we must use native traffic lights to get this feature.

This is not a thing on windows as far as I'm aware. The common practice (with electron too) seems to be to remove decorations entirely and create the titlebar from scratch using html/css/js

Yeah!
The reason why having this "floating" traffic lights feature on MacOS is so important, is that it's really complex, you have this dropdown when you hover over the maximize button etc. It's almost impossible to perfectly recreate this in html/css/js.

But window controls on windows and linux are way less complicated so you can realistically archive something that feels good on those platforms by using html/css/js

awkj commented

if want hide toolbar_button and let title transparent , I fix and complete a perfercet solution, thanks @JonasKruckenberg

use cocoa::appkit::{NSWindow, NSWindowStyleMask, NSWindowTitleVisibility};

pub trait WindowExt {
    #[cfg(target_os = "macos")]
    fn set_transparent_titlebar(&self, title_transparent: bool, remove_toolbar: bool);
}

impl<R: Runtime> WindowExt for Window<R> {
    #[cfg(target_os = "macos")]
    fn set_transparent_titlebar(&self, title_transparent: bool, remove_tool_bar: bool) {
        unsafe {
            let id = self.ns_window().unwrap() as cocoa::base::id;
            NSWindow::setTitlebarAppearsTransparent_(id, cocoa::base::YES);
            let mut style_mask = id.styleMask();
            style_mask.set(
                NSWindowStyleMask::NSFullSizeContentViewWindowMask,
                title_transparent,
            );

            if remove_tool_bar {
                style_mask.remove(
                    NSWindowStyleMask::NSClosableWindowMask
                        | NSWindowStyleMask::NSMiniaturizableWindowMask
                        | NSWindowStyleMask::NSResizableWindowMask,
                );
            }

            id.setStyleMask_(style_mask);

            id.setTitleVisibility_(if title_transparent {
                NSWindowTitleVisibility::NSWindowTitleHidden
            } else {
                NSWindowTitleVisibility::NSWindowTitleVisible
            });

            id.setTitlebarAppearsTransparent_(if title_transparent {
                cocoa::base::YES
            } else {
                cocoa::base::NO
            });
        }
    }
}
 let win = app.get_window("main").unwrap();
win.set_transparent_titlebar(true, false);

image

 let win = app.get_window("main").unwrap();
win.set_transparent_titlebar(true, true);

image

for removing traffic lights, from previous solution, window will not be resized or minimized manually or from Tauri API

I found another method for this (inspired from previous comment)

#[cfg(target_os = "macos")]
use cocoa::appkit::{NSWindow, NSWindowButton, NSWindowStyleMask, NSWindowTitleVisibility};

#[cfg(target_os = "macos")]
use objc::runtime::YES;

use tauri::{Runtime, Window};

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

pub trait WindowExt {
    #[cfg(target_os = "macos")]
    fn set_transparent_titlebar(&self, title_transparent: bool, remove_toolbar: bool);
}

impl<R: Runtime> WindowExt for Window<R> {
    #[cfg(target_os = "macos")]
    fn set_transparent_titlebar(&self, title_transparent: bool, remove_tool_bar: bool) {
        unsafe {
            let id = self.ns_window().unwrap() as cocoa::base::id;
            NSWindow::setTitlebarAppearsTransparent_(id, cocoa::base::YES);
            let mut style_mask = id.styleMask();
            style_mask.set(
                NSWindowStyleMask::NSFullSizeContentViewWindowMask,
                title_transparent,
            );

            id.setStyleMask_(style_mask);

            if remove_tool_bar {
                let close_button = id.standardWindowButton_(NSWindowButton::NSWindowCloseButton);
                let _: () = msg_send![close_button, setHidden: YES];
                let min_button = id.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton);
                let _: () = msg_send![min_button, setHidden: YES];
                let zoom_button = id.standardWindowButton_(NSWindowButton::NSWindowZoomButton);
                let _: () = msg_send![zoom_button, setHidden: YES];
            }

            id.setTitleVisibility_(if title_transparent {
                NSWindowTitleVisibility::NSWindowTitleHidden
            } else {
                NSWindowTitleVisibility::NSWindowTitleVisible
            });

            id.setTitlebarAppearsTransparent_(if title_transparent {
                cocoa::base::YES
            } else {
                cocoa::base::NO
            });
        }
    }
}
// see previous comment for usage and result
let win = app.get_window("main").unwrap();
win.set_transparent_titlebar(true, true);

add this in Cargo.toml

[target.'cfg(target_os = "macos")'.dependencies]
cocoa = "0.24"
objc = "0.2.7"

Hey. Is there anyway to make the titlebar inset? Should I make this a seperate issue? I've currently got this ↓

Screenshot 2022-05-23 at 08 47 36

electron/electron#33066 This could serve as a good guide for theming the native windows titlebar, it was implemented in an effort to bring Windows 11 features to the themed titlebar VSCode has.

if want hide toolbar_button and let title transparent , I fix and complete a perfercet solution, thanks @JonasKruckenberg

use cocoa::appkit::{NSWindow, NSWindowStyleMask, NSWindowTitleVisibility};

pub trait WindowExt {
    #[cfg(target_os = "macos")]
    fn set_transparent_titlebar(&self, title_transparent: bool, remove_toolbar: bool);
}

impl<R: Runtime> WindowExt for Window<R> {
    #[cfg(target_os = "macos")]
    fn set_transparent_titlebar(&self, title_transparent: bool, remove_tool_bar: bool) {
        unsafe {
            let id = self.ns_window().unwrap() as cocoa::base::id;
            NSWindow::setTitlebarAppearsTransparent_(id, cocoa::base::YES);
            let mut style_mask = id.styleMask();
            style_mask.set(
                NSWindowStyleMask::NSFullSizeContentViewWindowMask,
                title_transparent,
            );

            if remove_tool_bar {
                style_mask.remove(
                    NSWindowStyleMask::NSClosableWindowMask
                        | NSWindowStyleMask::NSMiniaturizableWindowMask
                        | NSWindowStyleMask::NSResizableWindowMask,
                );
            }

            id.setStyleMask_(style_mask);

            id.setTitleVisibility_(if title_transparent {
                NSWindowTitleVisibility::NSWindowTitleHidden
            } else {
                NSWindowTitleVisibility::NSWindowTitleVisible
            });

            id.setTitlebarAppearsTransparent_(if title_transparent {
                cocoa::base::YES
            } else {
                cocoa::base::NO
            });
        }
    }
}
 let win = app.get_window("main").unwrap();
win.set_transparent_titlebar(true, false);
image
 let win = app.get_window("main").unwrap();
win.set_transparent_titlebar(true, true);
image

Works well for main window and window created by WindowBuilder in command. But @awkj 's answer not worked in command.

metkm commented

Can we have this on windows too? Electron actually exposes an API titleBarOverlay that can be set to true with titleBarStyle to keep system native window controls and hide the titlebar.

const win = new BrowserWindow({
  titleBarStyle: 'hidden',
  titleBarOverlay: true
})

image

@JonasKruckenberg How to change the traffic light position, i found some information, but I don't understand object-c

https://github.com/patr0nus/DeskGap/blob/master/lib/src/platform/mac/BrowserWindow.mm#L86 https://github.com/electron/electron/blob/37d93b04824d4f0151b36edb867209b53272457f/shell/browser/native_window_mac.mm#L1529

i found answer:

id.setToolbar_(msg_send![class!(NSToolbar), new]);

https://github.com/lukakerr/NSWindowStyles#12-transparent-toolbar-without-seperator https://developer.apple.com/documentation/appkit/nstoolbar

I tried this which should work, but the app compiles fine and then just quits after id.init_() without any error. I don't know if that's some undefined behavior or a segfault.

  fn set_transparent_titlebar(&self, transparent: bool) {
        use cocoa::appkit::{NSToolbar, NSWindowTitleVisibility};

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

        unsafe {
            let toolbar = id.init_();

            println!("{:?}", toolbar);

            id.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden);

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

            id.setToolbar_(toolbar);
        }
    }

Edit: I did it yaaaaay :D. It's apparently not recommended to readjust the position completely. But almost all apps I saw like slack, discord, firefox etc. look like this.

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

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

        unsafe {
            let new_toolbar = NSToolbar::alloc(id);
            new_toolbar.init_();

            id.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden);

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

            id.setToolbar_(new_toolbar)
        }
    }
}

Screenshot 2022-07-26 at 15 32 33

Edit2: The part about other apps is wrong. There has to be a way to increase the padding even more.
Edit3: thats how they did it in electron. This is the commit.

Edit 4: Now it's working. I partly translated chunks from the electron source:

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

        let window = self.ns_window().unwrap() as cocoa::base::id;
        let traffic_light_offset_y = 30.;
        let traffic_light_offset_x = 50.;

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

            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 + traffic_light_offset_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 = traffic_light_offset_x + (i as f64 * space_between);
                button.setFrameOrigin(rect.origin);
            }

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

Unfortunately it jumps back when resizing the window or going into fullscreen. I guess this can simply be fixed with an event listener on window resize.

I deleted my previous comment as it became too cluttered.

A summary: I implemented custom traffic light offset and took inspiration from here.

    #[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(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");
}

The issue is that the buttons jump back whenever the window is resized. I tried to mitigate this by attaching a listener on window resize. That approach works but you can clearly see artifacts as the buttons get repositioned after the native renderer places them. This has to be fixed somehow.

Screenshot 2022-07-27 at 14 36 04

Edit: I talked with the people from the tauri discord. Apparently there is no way to intercept the native window rendering without modifying the tao source. I didn't quite get it, but maybe there is a way to specify the whole NSWindow upfront. I believe we are doing just that inside the setup() function. Maybe someone has an idea...
Edit2: I will open a new issue as this seems to be a larger topic.

If you want to make an artifact free version of the inset traffic lights, you can use this small (admittedly ugly) hack that I created:

use tauri::{Runtime, Window};

#[allow(dead_code)]
pub enum ToolbarThickness {
    Thick,
    Medium,
    Thin,
}

pub trait WindowExt {
    #[cfg(target_os = "macos")]
    fn set_transparent_titlebar(&self, thickness: ToolbarThickness);
}

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

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

            id.setTitlebarAppearsTransparent_(cocoa::base::YES);

            match thickness {
                ToolbarThickness::Thick => {
                    self.set_title("").expect("Title wasn't set to ''");
                    make_toolbar(id);
                }
                ToolbarThickness::Medium => {
                    id.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden);
                    make_toolbar(id);
                }
                ToolbarThickness::Thin => {
                    id.setTitleVisibility_(NSWindowTitleVisibility::NSWindowTitleHidden);
                }
            }
        }
    }
}

#[cfg(target_os = "macos")]
unsafe fn make_toolbar(id: cocoa::base::id) {
    use cocoa::appkit::{NSToolbar, NSWindow};

    let new_toolbar = NSToolbar::alloc(id);
    new_toolbar.init_();
    id.setToolbar_(new_toolbar);
}

Replace the one line in main from the previous comment to (or any thickness you like):

win.set_transparent_titlebar(ToolbarThickness::Thick);

This gives at least some control over the traffic light inset...

Screenshot 2022-08-04 at 14 19 08

Screenshot 2022-08-04 at 14 19 37

Screenshot 2022-08-04 at 14 20 09

@haasal So here's an interesting one, with your code and opening devtools, I get some crazy behavior on the front end in terms of divs disappearing whenever you resize the window... If I don't open dev tools, everything seems fine, Or vice versa, if I don't run set_transparent_titlebar and open dev tools, it runs fine... Weird! Is anyone else seeing this?

fnky commented

@tr3ysmith I can confirm this as well.

I didn’t test it but I have absolutely no idea what might be causing this. Someone else who knows more about tauri internals might have to look into this.

Edit: It isn’t really the solution that will make it into tauri anyways as it’s really hacky and not very customizable.

Did this ^ solve the issue with the disappearing divs?

@haasal
Excuse me, have you ever encountered a situation where the button's position print has changed after setting position_traffic_lights(x,y) in multiple windows, but the button position display on the other window has not changed (only main window work). I feel a little confused.


EDIT:
It turns out that it is not a multi-window problem, but window.set_title is called, whatever call first or call later, position_traffic_lights(x,y) will fail.

Using the code by @haasal from #2663 (comment) works great except if I put any buttons I put inside the (now empty/invisible) titlebar area they are having difficulties getting the click events through.

(The solution from #2663 (comment) works perfectly except the traffic lights are in the wrong place as I need a taller titlebar πŸ˜…)

Is there anything I could change in the titlebar constructor code to allow it to pass all clicks through?

I see that the issue has been closed because the macOS implementation has been merged. Since Windows 11 also brought more window tiling features into the title bar buttons, it might not be true anymore that HTML/CSS/JS will be enough to recreate a good platform's title bar experience.

But window controls on windows and linux are way less complicated so you can realistically archive something that feels good on those platforms by using html/css/js

I found this article that might be helpful to implement the native custom title bar overlay on windows: https://learn.microsoft.com/en-us/windows/apps/develop/title-bar

@lucasfernog Could you consider reopening the issue for windows implementation?

@shenghan97 That guide doesn't apply to Tauri. From the 2 options, simple and custom, the simple one can't do what this issue is talking about and the custom one is basically what we're doing/talking about: Completely removing the built-in titlebar and drawing one yourself.
If we have to draw it ourselves we might as well do it in html,css instead of using the win11 only apis (not even sure if we could use them in a win32 app like that)
The only thing missing is the Snap thingy on the maximize button which is tracked here #4531 which in turn is blocked by a WebView2 issue.

@FabianLars

Thank you! Good to know that! Although I'm not quite sure if the Full Customization section in the guide is talking about removing the title bar completely with the window buttons. The windows (caption) buttons are still controlled by the system.

It says in the guide:

You are responsible for drawing and input-handling for the entire window except the caption buttons, which are still provided by the window.

The system reserves the upper-left or upper-right corner of the app window for the system caption buttons (minimize, maximize/restore, close).

But the guide sadly indeed doesn't seem to apply to win32 applications.

fnky commented

Thank you for implementing this @probablykasper and thank you @lucasfernog for getting it in. Also thanks to everyone who contributed with their findings and workarounds.

I'm sure this issue will still be helpful for those who want more customisation out of their window title bars where configuration doesn't cover it.

I think the current change is a good start. I look forward to see more support like Electron window customization and some feature parity between macOS, Windows (and possibly GTK?)

Using the code by @haasal from #2663 (comment) works great except if I put any buttons I put inside the (now empty/invisible) titlebar area they are having difficulties getting the click events through.

(The solution from #2663 (comment) works perfectly except the traffic lights are in the wrong place as I need a taller titlebar πŸ˜…)

Is there anything I could change in the titlebar constructor code to allow it to pass all clicks through?

I have the same situation. Two solutions with two flaws. Anyone with better implementation?

Try this in tauri.conf.json

 {...
    {...

    "windows": [
      {
        "fullscreen": false,
        "height": 600,
        "resizable": true,
        "title": "",
        "width": 800,
        "titleBarStyle": "Overlay"
      }
    ]
  }
}

Is it actually possible now on macOS to define an inset for the traffic lights via an API or similar, not using that workaround from haasal?

Nope, the new built-in api does not support customizing the position yet.

It's August 2023, is there a solution for Windows now?

@zjprie094 well at least on Windows 11 there's dark theme titlebars

Try this in tauri.conf.json

 {...
    {...

    "windows": [
      {
        "fullscreen": false,
        "height": 600,
        "resizable": true,
        "title": "",
        "width": 800,
        "titleBarStyle": "Overlay"
      }
    ]
  }
}

The titleBarStyle property work but the window is not movable like it should be. Any solution?

@microSoftware With a disabled/overlayed titlebar style you now have to use the data-tauri-drag-region attribute to specify html elements that should act as your drag region (note that window.startDragging must be enabled on the allowlist, example: https://tauri.app/v1/guides/features/window-customization#tauriconfjson - it also shows the usage of the attribute itself). It's basically the same as if you disabled decorations as a whole.

Looks like TitleBarStyle is still unfortunately a macos exclusive, so you can't have your cake and eat it too, at least on Windows.

Looks like TitleBarStyle is still unfortunately a macos exclusive, so you can't have your cake and eat it too, at least on Windows.

Okay so how do i hide the title bar on windows then?

@microSoftware With a disabled/overlayed titlebar style you now have to use the data-tauri-drag-region attribute to specify html elements that should act as your drag region (note that window.startDragging must be enabled on the allowlist, example: https://tauri.app/v1/guides/features/window-customization#tauriconfjson - it also shows the usage of the attribute itself). It's basically the same as if you disabled decorations as a whole.

Sadly i use tauri 2.0 alpha and so many tauri.conf properties doesn't work like "startDragging" or allowList

To remove the title bar on windows, I used window_shadows crate and i also used this event to make the resizing way smoother than the default one:

window.on_window_event(|event| {
    match event {
        WindowEvent::Resized(..) => {
            std::thread::sleep(std::time::Duration::from_millis(1))
        }
        _ => {}
    }
});

@microSoftware

@microSoftware - I am on the same boat. It is kind of took a good time for me to figure out the 2.0 alpha does not support these settings in tauri.config. Anything you found out to get it work?

@microSoftware - I am on the same boat. It is kind of took a good time for me to figure out the 2.0 alpha does not support these settings in tauri.config. Anything you found out to get it work?

no sorry.