Las pestañas ocupan la fila del título también en Windows
En Windows la ventana conservaba la barra de título del sistema y la barra de pestañas iba DEBAJO: una fila entera gastada en repetir el nombre de la app. En macOS ya no pasaba (TitleBarStyle::Overlay desde la v1.4.2). Ahora las dos se comportan igual, y como Chrome: la barra de pestañas ES la fila del título. - Ventana sin decoración en Windows (decorations(false)). resizable(true) conserva WS_THICKFRAME, así que los bordes siguen redimensionando y Aero Snap (arrastrar a un borde, Win+flechas) sigue vivo. - La barra dibuja sus propios botones minimizar/maximizar/cerrar con las medidas y colores del sistema (46 px de ancho, alto completo de la barra, esquinas rectas, rojo #c42b1c al posarse en el aspa) y los glifos de Segoe Fluent Icons. Actúan por el evento nuevo ls-window; cerrar sigue pasando por CloseRequested, o sea que oculta a la bandeja igual que antes. - El icono del medio alterna maximizar/restaurar según el estado. El repintado se dispara solo cuando ese estado CAMBIA, no en cada píxel del redimensionado. - La geometría maximizada ya no se guarda entre sesiones: sin decoración, al reabrir volvería como ventana normal del tamaño de la pantalla y taparía la barra de tareas. Se conserva la última geometría restaurada. - Doble clic en el hueco libre de la barra ahora maximiza/restaura (lo gobierna data-tauri-drag-region), como la barra de título del sistema y como Chrome. Antes abría una pestaña nueva; para eso quedan el botón + y Cmd/Ctrl+T. NO incluye los Snap Layouts de Windows 11 (el menú que asoma al posar el cursor sobre maximizar). Requieren que la ventana conteste WM_NCHITTEST con HTMAXBUTTON, y aquí ese botón lo tapa el webview hijo de la barra, que se queda con los mensajes del ratón: la ventana principal nunca los ve. Es la misma razón por la que Electron necesita botones NATIVOS (titleBarOverlay) para poder ofrecerlo. El resto de Aero Snap no se ve afectado. Se añade .github/workflows/check.yml: cargo check en Windows y macOS al empujar una rama, sin publicar nada. Hasta ahora solo existía el workflow de publicar (etiquetas v*), así que no había forma de comprobar que algo compilaba sin sacar un release. Verificado: sintaxis del script de la barra con node --check; medidas, clases y estados de los botones comprobados en el navegador (46 px, pegados al borde, ocultos en macOS, icono y rótulo correctos al maximizar, y el arrastre solo en el hueco libre, nunca en los botones). La compilación la verifica el workflow nuevo. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Este commit está contenido en:
@@ -77,11 +77,14 @@ struct Shell {
|
||||
autostart: Option<bool>, // elección del usuario en el menú ⋮ (None = por defecto, activado)
|
||||
closed: Vec<String>, // URLs de pestañas cerradas (para «Reabrir pestaña cerrada», ⌘⇧T)
|
||||
menu_tab: Option<String>, // pestaña sobre la que se abrió el menú de clic derecho
|
||||
// Último estado maximizado conocido. Solo sirve para no repintar la barra en CADA
|
||||
// evento de redimensión: únicamente cuando el icono ▢/❐ tiene que cambiar.
|
||||
maximized: bool,
|
||||
}
|
||||
|
||||
impl Default for Shell {
|
||||
fn default() -> Self {
|
||||
Self { tabs: Vec::new(), active: 0, zoom_idx: ZOOM_100, next_id: 1, win: None, autostart: None, closed: Vec::new(), menu_tab: None }
|
||||
Self { tabs: Vec::new(), active: 0, zoom_idx: ZOOM_100, next_id: 1, win: None, autostart: None, closed: Vec::new(), menu_tab: None, maximized: false }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,6 +324,8 @@ fn current_zoom<R: Runtime>(app: &AppHandle<R>) -> f64 {
|
||||
|
||||
// Manda el estado de pestañas + zoom + versión a la barra (webview "chrome").
|
||||
fn broadcast<R: Runtime>(app: &AppHandle<R>) {
|
||||
// Se consulta ANTES de tomar el lock del shell: así no se anidan dos cerrojos.
|
||||
let maximized = main_window(app).and_then(|w| w.is_maximized().ok()).unwrap_or(false);
|
||||
let payload = {
|
||||
let shell = app.state::<ShellState>();
|
||||
let s = shell.lock().unwrap();
|
||||
@@ -335,8 +340,12 @@ fn broadcast<R: Runtime>(app: &AppHandle<R>) {
|
||||
"zoom": (ZOOM_STEPS[s.zoom_idx.min(ZOOM_STEPS.len()-1)] * 100.0).round() as u32,
|
||||
"version": app.package_info().version.to_string(),
|
||||
// En macOS la barra ocupa la fila del título (ventana con título en overlay) y
|
||||
// debe dejar libre el hueco del semáforo; en Windows hay título nativo aparte.
|
||||
// debe dejar libre el hueco del semáforo, que lo pinta el sistema.
|
||||
"mac": cfg!(target_os = "macos"),
|
||||
// En Windows la ventana va SIN decoración: la barra es la fila del título, así
|
||||
// que dibuja ella misma los botones minimizar/maximizar/cerrar (no hay nativos).
|
||||
"win": cfg!(target_os = "windows"),
|
||||
"maximized": maximized,
|
||||
})
|
||||
};
|
||||
let _ = app.emit_to(EventTarget::webview(CHROME), "ls-tabs", payload);
|
||||
@@ -933,10 +942,14 @@ pub fn run() {
|
||||
}
|
||||
|
||||
// Ventana principal (contenedora). Tamaño/posición de la sesión anterior si los hay.
|
||||
// macOS: título en OVERLAY y sin texto → el contenido (y por tanto la barra de
|
||||
// pestañas) ocupa también la fila del título, al estilo de Notion/VS Code. Los
|
||||
// botones del semáforo siguen siendo los nativos; la barra les deja su hueco.
|
||||
// En Windows se conserva el título del sistema con la barra debajo.
|
||||
// En AMBOS sistemas la barra de pestañas ocupa la fila del título, como Chrome:
|
||||
// no se desperdicia una franja entera en repetir el nombre de la app.
|
||||
// - macOS: título en OVERLAY y sin texto; el semáforo lo sigue pintando el sistema
|
||||
// encima y la barra le deja su hueco (clase `mac` del CSS).
|
||||
// - Windows: ventana SIN decoración; los botones minimizar/maximizar/cerrar los
|
||||
// dibuja la propia barra (clase `win`) y actúan por el evento `ls-window`.
|
||||
// `resizable(true)` conserva WS_THICKFRAME, así que los bordes siguen
|
||||
// redimensionando y Aero Snap (arrastrar a un borde, Win+flechas) sigue vivo.
|
||||
let mut wb = WindowBuilder::new(app, "main")
|
||||
.title("Labstream OS")
|
||||
.resizable(true)
|
||||
@@ -945,6 +958,10 @@ pub fn run() {
|
||||
{
|
||||
wb = wb.title_bar_style(tauri::TitleBarStyle::Overlay).hidden_title(true);
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
wb = wb.decorations(false);
|
||||
}
|
||||
match saved.win {
|
||||
Some(r) if r.w >= 600.0 && r.h >= 400.0 => {
|
||||
wb = wb.inner_size(r.w, r.h).position(r.x, r.y);
|
||||
@@ -1190,6 +1207,30 @@ pub fn run() {
|
||||
handle.listen_any("ls-menu", move |_| {
|
||||
show_app_menu(&h);
|
||||
});
|
||||
// Botones de ventana de la barra. Solo se pintan en Windows, donde la ventana va
|
||||
// sin decoración y no hay aspa del sistema. «close» pasa por CloseRequested, así
|
||||
// que hace lo mismo de siempre: ocultar a la bandeja, no salir de la app.
|
||||
let h = handle.clone();
|
||||
handle.listen_any("ls-window", move |e| {
|
||||
let Some(win) = main_window(&h) else { return };
|
||||
match field(e.payload(), "act").as_deref() {
|
||||
Some("min") => {
|
||||
let _ = win.minimize();
|
||||
}
|
||||
Some("max") => {
|
||||
// El repintado del icono ▢/❐ lo dispara el Resized que viene después.
|
||||
if win.is_maximized().unwrap_or(false) {
|
||||
let _ = win.unmaximize();
|
||||
} else {
|
||||
let _ = win.maximize();
|
||||
}
|
||||
}
|
||||
Some("close") => {
|
||||
let _ = win.close();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
// Clic en la etiqueta de versión de la barra → buscar actualización (con aviso).
|
||||
let h = handle.clone();
|
||||
handle.listen_any("ls-check-update", move |_| {
|
||||
@@ -1265,17 +1306,39 @@ pub fn run() {
|
||||
WindowEvent::Resized(_) | WindowEvent::ScaleFactorChanged { .. } => {
|
||||
let app = window.app_handle();
|
||||
layout_all(app);
|
||||
if let (Ok(pos), Ok(size)) = (window.outer_position(), window.inner_size()) {
|
||||
let scale = window.scale_factor().unwrap_or(1.0);
|
||||
let p = pos.to_logical::<f64>(scale);
|
||||
let s = size.to_logical::<f64>(scale);
|
||||
// Maximizar/restaurar llega como un Resized más: si el estado cambió, la
|
||||
// barra tiene que repintar su icono ▢/❐. Se compara antes de emitir para
|
||||
// no repintarla en cada píxel mientras se arrastra un borde.
|
||||
let maxi = window.is_maximized().unwrap_or(false);
|
||||
let cambio = {
|
||||
let shell = app.state::<ShellState>();
|
||||
shell.lock().unwrap().win =
|
||||
Some(WinRect { x: p.x, y: p.y, w: s.width, h: s.height });
|
||||
let mut s = shell.lock().unwrap();
|
||||
let antes = s.maximized;
|
||||
s.maximized = maxi;
|
||||
antes != maxi
|
||||
};
|
||||
if cambio {
|
||||
broadcast(app);
|
||||
}
|
||||
// La geometría MAXIMIZADA no se guarda: al reabrir volvería como ventana
|
||||
// normal del tamaño de la pantalla y, sin decoración, taparía la barra de
|
||||
// tareas. Se conserva la última geometría restaurada.
|
||||
if !maxi {
|
||||
if let (Ok(pos), Ok(size)) = (window.outer_position(), window.inner_size()) {
|
||||
let scale = window.scale_factor().unwrap_or(1.0);
|
||||
let p = pos.to_logical::<f64>(scale);
|
||||
let s = size.to_logical::<f64>(scale);
|
||||
let shell = app.state::<ShellState>();
|
||||
shell.lock().unwrap().win =
|
||||
Some(WinRect { x: p.x, y: p.y, w: s.width, h: s.height });
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowEvent::Moved(_) => {
|
||||
let app = window.app_handle();
|
||||
if window.is_maximized().unwrap_or(false) {
|
||||
return; // ver arriba: maximizada no se guarda
|
||||
}
|
||||
if let (Ok(pos), Ok(size)) = (window.outer_position(), window.inner_size()) {
|
||||
let scale = window.scale_factor().unwrap_or(1.0);
|
||||
let p = pos.to_logical::<f64>(scale);
|
||||
|
||||
Referencia en una nueva incidencia
Block a user