Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
fnky opened this issue Sep 26, 2021 · 54 comments
Closed

Comments

@fnky
Copy link

fnky commented Sep 26, 2021

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
Copy link

chhpt commented Jan 16, 2022

Any progress?

@gardc
Copy link
Contributor

gardc commented Jan 18, 2022

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

@FabianLars
Copy link
Member

@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! ❤️

@JonasKruckenberg JonasKruckenberg self-assigned this Jan 18, 2022
@JonasKruckenberg
Copy link
Member

JonasKruckenberg commented Jan 18, 2022

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

@fnky
Copy link
Author

fnky commented Jan 18, 2022

@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
Copy link

awkj commented Feb 13, 2022

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?

@JonasKruckenberg
Copy link
Member

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
Copy link

awkj commented Apr 7, 2022

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 !

@JonasKruckenberg
Copy link
Member

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
Copy link

awkj commented Apr 8, 2022

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

@JonasKruckenberg
Copy link
Member

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
Copy link

awkj commented Apr 8, 2022

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
Copy link

awkj commented Apr 9, 2022

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>

@JonasKruckenberg
Copy link
Member

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
Copy link

awkj commented Apr 10, 2022

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.

@xuchaoqian
Copy link
Contributor

xuchaoqian commented May 11, 2022

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

@JonasKruckenberg
Copy link
Member

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

@xuchaoqian
Copy link
Contributor

xuchaoqian commented May 11, 2022

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

@JonasKruckenberg
Copy link
Member

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
Copy link

awkj commented May 11, 2022

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

@RtthLvr
Copy link

RtthLvr commented May 19, 2022

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"

@Lalit64
Copy link

Lalit64 commented May 23, 2022

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

@MulverineX
Copy link

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.

@mantou132
Copy link

mantou132 commented Jun 9, 2022

@haasal
Copy link

haasal commented Aug 14, 2022

Did this ^ solve the issue with the disappearing divs?

@Moon1102
Copy link

Moon1102 commented Sep 6, 2022

@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.

@Uninen
Copy link
Contributor

Uninen commented Sep 26, 2022

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?

@shenghan97
Copy link

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?

@FabianLars
Copy link
Member

@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.

@shenghan97
Copy link

@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
Copy link
Author

fnky commented Nov 2, 2022

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?)

@Mehdi-Hp
Copy link

Mehdi-Hp commented Nov 8, 2022

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?

@eli-front
Copy link

Try this in tauri.conf.json

 {...
    {...

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

@EtzBetz
Copy link

EtzBetz commented Dec 21, 2022

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?

@FabianLars
Copy link
Member

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

@schickling
Copy link

See #4789

@zjprie094
Copy link

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

@MulverineX
Copy link

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

@microSoftware
Copy link

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?

@FabianLars
Copy link
Member

@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.

@MulverineX
Copy link

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

@microSoftware
Copy link

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
Copy link

@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

@kleenkanteen
Copy link

kleenkanteen commented Sep 19, 2023

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

@kumareshind
Copy link

@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
Copy link

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests