Skip to content
271 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again.1//! The window's frame. g1t draws its own title bar, as Slack and Discord
2//! do, so the window is all g1t and looks the same on every system: on
3//! Windows and Linux the system's frame goes and the bar has the menu
4//! button, back and forward, search, and minimize, maximize and close; on
Merge branch 'main' of github.com:flagon-io/g1t5//! macOS the bar sits under the window's own traffic lights.
6//!
7//! The main window holds two web views, one above the other: the bar, a
8//! page bundled with the app (`launcher/titlebar.html`), across the top,
9//! and the site under it. The site is never drawn under the bar, so no page
10//! of it, or of GitHub's while signing in, needs to know the bar is there.
11//! What the bar shows of the page (its title, its search, whether there is
12//! a way back, its colours) the page's bridge reports (`page`), and what
13//! the bar is asked to do comes back to the page through the app (`act`).
14//!
15//! Settings can give the window back its system frame, and the menu bar
16//! with it, from the next start; the site then has the window to itself.
17
18use std::sync::Mutex;
The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again.19
Merge branch 'main' of github.com:flagon-io/g1t20use serde::{Deserialize, Serialize};
21use tauri::webview::WebviewBuilder;
22use tauri::window::{Color, WindowBuilder};
23use tauri::{AppHandle, Emitter, EventTarget, LogicalPosition, LogicalSize, Manager, Runtime, Webview, WebviewUrl, WebviewWindowBuilder, Window};
The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again.24
25use crate::{WINDOW, config, tray};
26
Merge branch 'main' of github.com:flagon-io/g1t27/// The bar's height, in CSS pixels.
The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again.28pub const HEIGHT: f64 = 36.0;
29
Merge branch 'main' of github.com:flagon-io/g1t30/// The site's web view, in the main window.
31pub const SITE: &str = "site";
32/// The bar's web view, across the top of the main window.
33pub const BAR: &str = "titlebar";
34
35/// What the bar listens for: what to show (`Page`).
36pub const PAGE_EVENT: &str = "g1t-title-bar";
37/// What the site listens for: the bar's search was clicked.
38pub const SEARCH_EVENT: &str = "g1t-search";
39
40/// g1t's rail, dark, which the window is painted while a page loads.
41const RAIL: Color = Color(5, 5, 7, 255);
42
The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again.43/// Whether g1t draws its own title bar, as the settings say now.
44pub fn custom(app: &AppHandle) -> bool {
45 !config::settings(app).system_title_bar
46}
47
48/// The title bar g1t started with, which its windows keep until it restarts.
49static STARTED_CUSTOM: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
50
51/// Whether this run draws g1t's own title bar: decided once, at the start.
52pub fn started(app: &AppHandle) -> bool {
53 *STARTED_CUSTOM.get_or_init(|| custom(app))
54}
55
Merge branch 'main' of github.com:flagon-io/g1t56/// The main window, holding the bar and the site.
57pub fn window(app: &AppHandle) -> Option<Window> {
58 app.get_window(WINDOW)
59}
60
61/// The site's web view.
62pub fn site(app: &AppHandle) -> Option<Webview> {
63 app.get_webview(SITE)
64}
65
66/// The main window, before its web views: g1t's frame, or the system's.
67pub fn main_window<'a, R: Runtime, M: Manager<R>>(app: &'a M, custom: bool) -> tauri::Result<WindowBuilder<'a, R, M>> {
68 #[cfg(target_os = "macos")]
69 let builder = if custom {
70 // The traffic lights stay, over the bar and centred in it.
71 let config = tauri::utils::config::WindowConfig {
72 label: WINDOW.into(),
73 traffic_light_position: Some(tauri::utils::config::LogicalPosition { x: 14.0, y: (HEIGHT - 14.0) / 2.0 }),
74 ..Default::default()
75 };
76 WindowBuilder::from_config(app, &config)?.title_bar_style(tauri::TitleBarStyle::Overlay).hidden_title(true)
77 } else {
78 WindowBuilder::new(app, WINDOW)
79 };
80 #[cfg(not(target_os = "macos"))]
81 let builder = WindowBuilder::new(app, WINDOW).decorations(!custom);
82 Ok(builder.title("g1t").inner_size(1280.0, 840.0).min_inner_size(720.0, 480.0).visible(false))
83}
84
85/// The bar's and the site's web views, in the main window, as tall as the window leaves them.
86pub fn fill(window: &Window, custom: bool, site: WebviewBuilder<tauri::Wry>) -> tauri::Result<()> {
87 let size = logical_size(window);
88 let top = if custom { HEIGHT } else { 0.0 };
89 if custom {
90 window.add_child(
91 WebviewBuilder::new(BAR, WebviewUrl::App("titlebar.html".into())).background_color(RAIL).on_navigation(config::is_launcher),
92 LogicalPosition::new(0.0, 0.0),
93 LogicalSize::new(size.width, HEIGHT),
94 )?;
95 }
96 window.add_child(site.background_color(RAIL), LogicalPosition::new(0.0, top), LogicalSize::new(size.width, (size.height - top).max(0.0)))?;
97 #[cfg(target_os = "linux")]
98 if custom {
99 pack_bar(window);
100 }
101 Ok(())
102}
103
104/// On Linux the web views sit in a GTK box, one under the other, which
105/// shares the window between them equally whatever size they are given:
106/// the bar is told to keep its height, and the site takes the rest.
107#[cfg(target_os = "linux")]
108fn pack_bar(window: &Window) {
109 use gtk::prelude::*;
110 let Ok(vbox) = window.default_vbox() else { return };
111 // The bar is the first web view added; the menu bar, hidden, is not one.
112 if let Some(bar) = vbox.children().into_iter().find(|child| child.type_().name() == "WebKitWebView") {
113 vbox.set_child_packing(&bar, false, true, 0, gtk::PackType::Start);
114 bar.set_size_request(-1, HEIGHT as i32);
115 }
116}
117
118fn logical_size(window: &Window) -> LogicalSize<f64> {
119 let scale = window.scale_factor().unwrap_or(1.0);
120 window.inner_size().map(|size| size.to_logical(scale)).unwrap_or(LogicalSize::new(1280.0, 840.0))
121}
122
123/// The window changed size: the bar keeps its height, and the site takes the rest.
124pub fn layout(window: &Window) {
125 let app = window.app_handle();
126 let size = logical_size(window);
127 let bar = app.get_webview(BAR);
128 let top = if bar.is_some() { HEIGHT } else { 0.0 };
129 if let Some(bar) = bar {
130 let _ = bar.set_position(LogicalPosition::new(0.0, 0.0));
131 let _ = bar.set_size(LogicalSize::new(size.width, HEIGHT));
132 }
133 if let Some(site) = site(app) {
134 let _ = site.set_position(LogicalPosition::new(0.0, top));
135 let _ = site.set_size(LogicalSize::new(size.width, (size.height - top).max(0.0)));
136 }
137}
138
139/// The settings window: g1t's frame, or the system's. Its page draws its
140/// own bar (launcher/settings.html), being g1t's own.
141pub fn dialog<'a, R: Runtime, M: Manager<R>>(builder: WebviewWindowBuilder<'a, R, M>, custom: bool) -> WebviewWindowBuilder<'a, R, M> {
The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again.142 if !custom {
143 return builder;
144 }
145 #[cfg(target_os = "macos")]
146 {
147 builder
148 .title_bar_style(tauri::TitleBarStyle::Overlay)
149 .hidden_title(true)
150 .traffic_light_position(LogicalPosition::new(14.0, (HEIGHT - 14.0) / 2.0))
151 }
152 #[cfg(not(target_os = "macos"))]
153 {
154 builder.decorations(false)
155 }
156}
157
158/// The main window's menu bar: shown under the system's frame, and kept
159/// hidden under g1t's, whose menu button opens the same menu (`menu`) and
160/// whose shortcuts still work.
Merge branch 'main' of github.com:flagon-io/g1t161pub fn menu_bar(window: &Window, custom: bool) {
The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again.162 let _ = if custom { window.hide_menu() } else { window.show_menu() };
163 // GTK shows everything in a window each time the window is shown, the
164 // hidden menu bar too, unless the menu bar is told to sit that out.
165 #[cfg(target_os = "linux")]
166 if let Ok(vbox) = window.default_vbox() {
167 use gtk::prelude::*;
168 for child in vbox.children().into_iter().filter(|child| child.is::<gtk::MenuBar>()) {
169 child.set_no_show_all(custom);
170 }
171 }
172}
173
Merge branch 'main' of github.com:flagon-io/g1t174/// What a window button asks of the window it is in: `minimize`,
175/// `maximize` (which toggles), `close`, `drag`, or `maximized` to ask.
176/// Answers whether the window is maximized.
177pub fn control(app: &AppHandle, window: &Window, action: &str) -> Result<bool, String> {
The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again.178 let main = window.label() == WINDOW;
179 match action {
180 "minimize" => window.minimize().map_err(|error| error.to_string())?,
181 "maximize" => {
182 let maximized = window.is_maximized().unwrap_or(false);
183 let _ = if maximized { window.unmaximize() } else { window.maximize() };
184 }
185 "close" if main => tray::close(app),
186 "close" => window.close().map_err(|error| error.to_string())?,
187 "drag" => window.start_dragging().map_err(|error| error.to_string())?,
188 "maximized" => {}
189 other => return Err(format!("the window cannot {other}")),
190 }
191 Ok(window.is_maximized().unwrap_or(false))
192}
193
194/// The menu button: the window's menu, under the button.
Merge branch 'main' of github.com:flagon-io/g1t195pub fn menu(window: &Window, x: f64, y: f64) -> Result<(), String> {
The desktop app draws its own title bar, as Slack and Discord do, and the site can call the app again.196 #[cfg(target_os = "macos")]
197 let menu = window.app_handle().menu();
198 #[cfg(not(target_os = "macos"))]
199 let menu = window.menu();
200 let menu = menu.ok_or("the window has no menu")?;
201 window.popup_menu_at(&menu, LogicalPosition::new(x, y)).map_err(|error| error.to_string())
202}
Merge branch 'main' of github.com:flagon-io/g1t203
204/// The page's colours, as the bar takes them: CSS colours, resolved.
205#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
206pub struct Colors {
207 pub background: String,
208 pub foreground: String,
209 pub muted: String,
210 pub accent: String,
211 /// Whether the page is dark, for the bar's own hover shades.
212 pub dark: bool,
213}
214
215/// What the bar shows of the page in the window under it.
216#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
217#[serde(rename_all = "camelCase")]
218pub struct Page {
219 pub title: String,
220 /// The site's search, by its label ("Search Flagon"), when it has handed one over.
221 pub search: Option<String>,
222 /// Whether there is a page to go back or forward to; absent where the web view cannot tell.
223 pub can_go_back: Option<bool>,
224 pub can_go_forward: Option<bool>,
225 pub colors: Option<Colors>,
226}
227
228/// The page as last reported, kept for a bar that loads after it.
229#[derive(Default)]
230pub struct State(Mutex<Page>);
231
232/// The site's page reported what it is: the bar is told.
233pub fn page(app: &AppHandle, mut page: Page) {
234 page.title.truncate(300);
235 if let Some(search) = page.search.as_mut() {
236 search.truncate(120);
237 }
238 let state = app.state::<State>();
239 let mut kept = state.0.lock().unwrap();
240 // A page that knows no colours of its own keeps the last ones.
241 if page.colors.is_none() {
242 page.colors = kept.colors.clone();
243 }
244 if *kept == page {
245 return;
246 }
247 *kept = page.clone();
248 drop(kept);
249 let _ = app.emit_to(EventTarget::webview(BAR), PAGE_EVENT, page);
250}
251
252/// What the bar shows now: it asks as it loads.
253pub fn current(app: &AppHandle) -> Page {
254 app.state::<State>().0.lock().unwrap().clone()
255}
256
257/// A button on the bar that acts on the page: `back`, `forward`, `search`,
258/// or `focus`, which hands the keyboard back to the page after a click on
259/// the bar.
260pub fn act(app: &AppHandle, action: &str) -> Result<(), String> {
261 let site = site(app).ok_or("there is no page")?;
262 match action {
263 "back" => site.eval("history.back()").map_err(|error| error.to_string())?,
264 "forward" => site.eval("history.forward()").map_err(|error| error.to_string())?,
265 "search" => app.emit_to(EventTarget::webview(SITE), SEARCH_EVENT, ()).map_err(|error| error.to_string())?,
266 "focus" => {}
267 other => return Err(format!("the bar cannot {other}")),
268 }
269 let _ = site.set_focus();
270 Ok(())
271}

This file's history is long; its oldest lines are credited to the oldest commit read.