mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-22 16:02:24 +00:00
feat(chrome): make the title bar's line whole off macOS
macOS fills the window's leading corner with the traffic lights and `TITLE_BAR_LEAD` reserves them 80px. Everywhere else that corner held nothing: the caption row's only contents are the rail's "+" and collapse at the rail's right edge and the corner chrome at the window's, so the left third of the row read as unfinished rather than restrained — while Windows treats the top-left as the app's identity slot. Three parts, all of them about that row: - `window_mark()` draws the "duo" mark (the app icon's own art) at the head of the rail on `CONTENT_INSET`, the line the search box and every row label below it start on, and follows the rail's controls into the title strip when the sidebar collapses. It is drawn, never clicked: no hover capsule, and deliberately no `occlude()`, so the drag region underneath still takes the press and the strip stays grabbable. - The rail's stand-in row now reserves the same hairline the real `TitleBar` draws inside its own height. Without it the bar centred content on 19.5 and the rail on 20, and the mark hopped half a pixel as collapsing the rail handed it from one to the other. - With the detail panel open off macOS the bar is hoisted above `[terminal | panel]` so the window controls can reach the corner, which left the code and diff overlays — anchored to the terminal column — starting 40px down, with headers drawn to *be* the title bar landing a row low. They now hang on the row that owns the bar, inset by the panel's width. Covering the caption row that way needs the headers to carry its gestures, which neither ever did with the panel open or closed: `title_bar_drag()` gives both (and the rail's row, which grew the same wiring by hand) drag-to-move and double-click-to-zoom, and their controls are `occlude()`d so HTCAPTION stops eating the clicks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WCb8ZDmvdA5xbVtvs647tD
This commit is contained in:
@@ -7,8 +7,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- **The window's leading corner carries the app's mark off macOS** — macOS fills
|
||||
the top-left with the traffic lights; on Windows and Linux that corner was
|
||||
empty, with everything the caption row holds (the rail's "+" and collapse, the
|
||||
corner chrome, the window controls) pushed to a right edge. The "duo" mark now
|
||||
heads the tab rail on its content inset, the line the search box and every row
|
||||
label below it start on, and follows the rail's controls into the title strip
|
||||
when the sidebar is collapsed — so the corner never falls back to nothing.
|
||||
Drawn, never clicked: it takes no hover capsule and no hit box, leaving the
|
||||
strip grabbable through it.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The editor and diff overlays keep their header on the caption line when the
|
||||
detail panel is open** — off macOS the title bar is hoisted above
|
||||
`[terminal | panel]` so the ─ ▢ ✕ group can reach the window's corner, which
|
||||
left both overlays — anchored to the terminal column — starting 40px down.
|
||||
Their headers are drawn to *be* the title bar while they're up (its height,
|
||||
its insets, a full chrome tile for their one control), and instead landed a
|
||||
row low, level with the panel's tab row. They now hang on the row that owns
|
||||
the bar, inset by the panel's width, so the corner chrome keeps its surface
|
||||
and its clicks.
|
||||
- **Those headers became real title bars** — dragging one now moves the window
|
||||
and double-clicking zooms it. Both covered the caption row and neither did
|
||||
either, with the panel open or closed, so opening a file turned the top of the
|
||||
window into a 40px strip that looked exactly like a title bar and answered
|
||||
nothing. Their controls (the ✕, the diff's back-to-all-files chip) are
|
||||
`occlude()`d to keep taking clicks: a drag region on Windows is HTCAPTION, and
|
||||
the OS claims the press before the app hit-tests.
|
||||
- **The rail's top zone lines up with the title bar to the pixel** — the bar
|
||||
reserves a hairline inside its own height that the rail's stand-in row didn't,
|
||||
so everything in that row sat half a pixel low. Invisible on the line-art
|
||||
tiles; not on the mark, which visibly hopped as collapsing the rail handed it
|
||||
over to the bar.
|
||||
- **CJK and emoji stop falling through to the OS on Windows and Linux** — the
|
||||
default `font_fallbacks` named only faces that ship with macOS (Menlo, Apple
|
||||
Color Emoji), so off macOS the entire chain matched nothing and every
|
||||
|
||||
+112
-2
@@ -9,7 +9,9 @@ use gpui_component::color_picker::{ColorPickerEvent, ColorPickerState};
|
||||
use gpui_component::input::{InputEvent, InputState};
|
||||
use gpui_component::select::{SearchableVec, SelectEvent, SelectState};
|
||||
use gpui_component::slider::{SliderEvent, SliderState};
|
||||
use gpui_component::{ActiveTheme as _, IndexPath, TitleBar, WindowExt as _};
|
||||
use gpui_component::{
|
||||
ActiveTheme as _, IndexPath, InteractiveElementExt as _, TitleBar, WindowExt as _,
|
||||
};
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::HashSet;
|
||||
use std::rc::Rc;
|
||||
@@ -190,6 +192,79 @@ pub(crate) fn title_bar_hug_offset() -> f32 {
|
||||
}
|
||||
}
|
||||
|
||||
/// Edge of the brand mark that anchors the window's leading corner off macOS
|
||||
/// (see [`window_mark`]). Between a chrome tile's 32px hit box and its 13px
|
||||
/// glyph: the mark paints no hover capsule, so what has to sit level with the
|
||||
/// tiles beside it is its *ink* — and solid art reads heavier than line work at
|
||||
/// equal size, hence short of the tile box rather than matching it.
|
||||
pub(crate) const WINDOW_MARK_SIZE: f32 = 20.;
|
||||
|
||||
/// The "duo" mark — the same art the app icon and the About page carry — drawn
|
||||
/// at the leading edge of the title-bar row, or `None` on macOS.
|
||||
///
|
||||
/// macOS owns that corner: the traffic lights sit there, and [`TITLE_BAR_LEAD`]
|
||||
/// reserves them 80px. Everywhere else it is empty. The row's contents are the
|
||||
/// rail's controls at its *right* end and the window chrome at the far side, so
|
||||
/// the window's leading corner — the slot Windows reads as the app's identity,
|
||||
/// filled by Explorer, VS Code and Zed alike — held nothing at all, which comes
|
||||
/// across as unfinished rather than restrained.
|
||||
///
|
||||
/// Drawn, never clicked. It is not a menu button, so it stays out of the tile
|
||||
/// rhythm (no hover capsule) and deliberately takes no `occlude()`: the row it
|
||||
/// lives in is a `WindowControlArea::Drag`, and letting the mark fall through to
|
||||
/// that keeps the strip grabbable instead of punching a dead 20px hole in it.
|
||||
/// Make a row that stands in for the title bar behave like one: drag it to move
|
||||
/// the window, double-click it to zoom.
|
||||
///
|
||||
/// Three rows do this. The rail's top zone sits level with the real bar but
|
||||
/// outside it (the bar only spans the column beside the rail), and the code and
|
||||
/// diff overlays each cover the bar with a header of their own drawn to its line.
|
||||
/// Without this they are all dead strips: 40px across the top of the window that
|
||||
/// look exactly like the caption and do nothing when you grab them.
|
||||
///
|
||||
/// Driven the way gpui-component's own `TitleBar` drives it — a press arms a
|
||||
/// flag and the first *move* starts the window move — so a plain click, and a
|
||||
/// double-click, still land intact. Note that on Windows the drag area maps to
|
||||
/// HTCAPTION and the OS claims the press before gpui hit-tests, so every button
|
||||
/// inside one of these rows needs an `occlude()` wrapper to get its clicks back.
|
||||
pub(crate) fn title_bar_drag(row: gpui::Stateful<gpui::Div>) -> gpui::Stateful<gpui::Div> {
|
||||
let should_move = Rc::new(Cell::new(false));
|
||||
row.window_control_area(gpui::WindowControlArea::Drag)
|
||||
.on_mouse_down(gpui::MouseButton::Left, {
|
||||
let should_move = should_move.clone();
|
||||
move |_, _, _| should_move.set(true)
|
||||
})
|
||||
.on_mouse_up(gpui::MouseButton::Left, {
|
||||
let should_move = should_move.clone();
|
||||
move |_, _, _| should_move.set(false)
|
||||
})
|
||||
.on_mouse_move(move |_, window, _| {
|
||||
if should_move.replace(false) {
|
||||
window.start_window_move();
|
||||
}
|
||||
})
|
||||
.on_double_click(|_, window, _| window.titlebar_double_click())
|
||||
}
|
||||
|
||||
pub(crate) fn window_mark() -> Option<impl IntoElement> {
|
||||
if cfg!(target_os = "macos") {
|
||||
return None;
|
||||
}
|
||||
// Decoded once and shared: the title bar re-renders on every cursor blink,
|
||||
// and building a fresh `Image` per frame would re-copy the PNG and miss
|
||||
// gpui's image cache, which is keyed on the image's identity.
|
||||
static LOGO: std::sync::OnceLock<Arc<gpui::Image>> = std::sync::OnceLock::new();
|
||||
let logo = LOGO
|
||||
.get_or_init(|| {
|
||||
Arc::new(gpui::Image::from_bytes(
|
||||
gpui::ImageFormat::Png,
|
||||
include_bytes!("../../assets/logo@256.png").to_vec(),
|
||||
))
|
||||
})
|
||||
.clone();
|
||||
Some(img(logo).size(px(WINDOW_MARK_SIZE)).flex_shrink_0())
|
||||
}
|
||||
|
||||
/// One tab: a split-pane tree plus an optional user-assigned name. Settings is
|
||||
/// no longer a tab — it's a full-window overlay (`Tty7App::settings`), so every
|
||||
/// tab is a real terminal tab.
|
||||
@@ -5198,6 +5273,26 @@ impl Render for Tty7App {
|
||||
} else {
|
||||
(Some(title_bar), None)
|
||||
};
|
||||
// And where the overlays hang. Normally on the terminal column, which they
|
||||
// fill: the bar is that column's first child, so an `inset_0` overlay
|
||||
// covers it and the overlay's own header row lands *on* the caption line —
|
||||
// which is what both headers are drawn for (title-bar height, the bar's
|
||||
// insets, a full-size chrome tile for their one control).
|
||||
//
|
||||
// With the bar hoisted, a column-anchored overlay starts 40px down and its
|
||||
// header sits one row too low: level with the panel's tab row instead of
|
||||
// with the caption. So it hangs on the row that owns the bar instead,
|
||||
// inset from the right by the panel's width — covering the bar's band over
|
||||
// the terminal column (which carries nothing there but the drag region, or
|
||||
// the rail's controls while it's collapsed: exactly what an overlay covers
|
||||
// with the panel closed) and stopping short of the panel, so the ─ ▢ ✕
|
||||
// group and the corner chrome keep their own surface and their clicks.
|
||||
let (column_overlays, hoisted_overlays) = if panel_below_title_bar {
|
||||
(Vec::new(), overlays)
|
||||
} else {
|
||||
(overlays, Vec::new())
|
||||
};
|
||||
let panel_px = self.right_panel_px(window, cx);
|
||||
// The terminal column, and the anchor for both overlays: they fill it —
|
||||
// and, since the panel is a sibling rather than a child, stop short of the
|
||||
// panel for free. With the bar spanning above, they stop short of it too,
|
||||
@@ -5211,7 +5306,7 @@ impl Render for Tty7App {
|
||||
.relative()
|
||||
.when_some(column_title_bar, |this, bar| this.child(bar))
|
||||
.child(body_area)
|
||||
.children(overlays);
|
||||
.children(column_overlays);
|
||||
let panel_row = div()
|
||||
.flex_1()
|
||||
.min_h_0()
|
||||
@@ -5233,6 +5328,8 @@ impl Render for Tty7App {
|
||||
.min_w_0()
|
||||
.flex()
|
||||
.flex_col()
|
||||
// The containing block for the hoisted overlays below.
|
||||
.relative()
|
||||
.child(
|
||||
// The bar's own band over the panel, painted in the panel's
|
||||
// surface so the column still reads as one continuous
|
||||
@@ -5265,6 +5362,19 @@ impl Render for Tty7App {
|
||||
.child(bar),
|
||||
)
|
||||
.child(panel_row)
|
||||
// Last child, so they paint over both the bar and the column.
|
||||
// Each overlay is `absolute().inset_0()` against this wrapper,
|
||||
// which is the only thing that has to know where the panel
|
||||
// starts.
|
||||
.children(hoisted_overlays.into_iter().map(|overlay| {
|
||||
div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.bottom_0()
|
||||
.right(px(panel_px))
|
||||
.child(overlay)
|
||||
}))
|
||||
.into_any_element(),
|
||||
None => panel_row.into_any_element(),
|
||||
})
|
||||
|
||||
+26
-18
@@ -801,7 +801,7 @@ impl Tty7App {
|
||||
/// every buffer that was ever opened. Sits on the title bar's line and matches
|
||||
/// its height, so the editor's top edge lines up with the panel's tab row and
|
||||
/// the rail's controls across the window.
|
||||
fn render_editor_header(&self, cx: &mut Context<Self>) -> gpui::Div {
|
||||
fn render_editor_header(&self, cx: &mut Context<Self>) -> gpui::Stateful<gpui::Div> {
|
||||
let active = self.tab_code().and_then(|c| c.active_file());
|
||||
let name = active.map(|f| f.label());
|
||||
let dirty = active.is_some_and(|f| f.dirty);
|
||||
@@ -816,7 +816,10 @@ impl Tty7App {
|
||||
} else {
|
||||
crate::ui::app::TITLE_BAR_LEAD
|
||||
};
|
||||
h_flex()
|
||||
// The overlay covers the real title bar, so this row inherits its drag and
|
||||
// zoom gestures — otherwise opening a file turns the top of the window into
|
||||
// a strip that looks like the caption and can't move it.
|
||||
crate::ui::app::title_bar_drag(h_flex().id("editor-header"))
|
||||
.flex_none()
|
||||
.h(px(crate::ui::app::TITLE_BAR_HEIGHT))
|
||||
.items_center()
|
||||
@@ -847,22 +850,27 @@ impl Tty7App {
|
||||
)
|
||||
})
|
||||
.child(
|
||||
crate::ui::tab_strip::chrome_tile_sized(
|
||||
// This header is the title bar's own height and sits flush
|
||||
// with it, so its one control is a full chrome tile — not the
|
||||
// half-size one it used to be, which read as a different
|
||||
// class of button on the same line.
|
||||
Button::new("editor-panel-close").icon(Icon::new(IconName::Close)),
|
||||
crate::ui::app::TILE_SIZE,
|
||||
crate::ui::app::TILE_GLYPH_LINE,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
.rounded_lg()
|
||||
.tooltip("Back to Terminal (Esc)")
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.toggle_code_panel(window, cx);
|
||||
})),
|
||||
// `occlude()` for the same reason the title bar's own tiles carry
|
||||
// it: this row is a `WindowControlArea::Drag`, which on Windows is
|
||||
// HTCAPTION, and the OS takes the press before gpui hit-tests.
|
||||
div().occlude().flex_shrink_0().child(
|
||||
crate::ui::tab_strip::chrome_tile_sized(
|
||||
// This header is the title bar's own height and sits flush
|
||||
// with it, so its one control is a full chrome tile — not the
|
||||
// half-size one it used to be, which read as a different
|
||||
// class of button on the same line.
|
||||
Button::new("editor-panel-close").icon(Icon::new(IconName::Close)),
|
||||
crate::ui::app::TILE_SIZE,
|
||||
crate::ui::app::TILE_GLYPH_LINE,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
.rounded_lg()
|
||||
.tooltip("Back to Terminal (Esc)")
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.toggle_code_panel(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+56
-47
@@ -327,7 +327,10 @@ impl Tty7App {
|
||||
} else {
|
||||
crate::ui::app::TITLE_BAR_LEAD
|
||||
};
|
||||
h_flex()
|
||||
// Standing in for the title bar means carrying its gestures too: the
|
||||
// overlay covers the real bar, so without this the whole top of the window
|
||||
// stops moving it while a diff is up.
|
||||
crate::ui::app::title_bar_drag(h_flex().id("diff-overlay-header"))
|
||||
.flex_shrink_0()
|
||||
.h(px(crate::ui::app::TITLE_BAR_HEIGHT))
|
||||
.pl(px(lead))
|
||||
@@ -355,38 +358,42 @@ impl Tty7App {
|
||||
// click target back to the whole tree — otherwise the only way out
|
||||
// of a focused view would be to close and re-open the overlay.
|
||||
.when_some(focused_name(overlay), |bar, name| {
|
||||
// Wrapped like every other control on a drag row — see the header's
|
||||
// own note: HTCAPTION would otherwise swallow the click on Windows.
|
||||
bar.child(
|
||||
h_flex()
|
||||
.id("diff-overlay-unfocus")
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.px_1p5()
|
||||
.py_0p5()
|
||||
.rounded_md()
|
||||
.cursor_pointer()
|
||||
.hover(|s| s.bg(cx.theme().list_hover))
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
let active = this.active;
|
||||
if let Some(overlay) = this
|
||||
.tabs
|
||||
.get_mut(active)
|
||||
.and_then(|t| t.diff_overlay.as_mut())
|
||||
{
|
||||
overlay.focus = None;
|
||||
cx.notify();
|
||||
}
|
||||
}))
|
||||
.child(
|
||||
Icon::new(IconName::ChevronLeft)
|
||||
.small()
|
||||
.text_color(cx.theme().muted_foreground),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.font_family(self.font_family.clone())
|
||||
.child(name),
|
||||
),
|
||||
div().occlude().flex_shrink_0().child(
|
||||
h_flex()
|
||||
.id("diff-overlay-unfocus")
|
||||
.items_center()
|
||||
.gap_1()
|
||||
.px_1p5()
|
||||
.py_0p5()
|
||||
.rounded_md()
|
||||
.cursor_pointer()
|
||||
.hover(|s| s.bg(cx.theme().list_hover))
|
||||
.on_click(cx.listener(|this, _, _window, cx| {
|
||||
let active = this.active;
|
||||
if let Some(overlay) = this
|
||||
.tabs
|
||||
.get_mut(active)
|
||||
.and_then(|t| t.diff_overlay.as_mut())
|
||||
{
|
||||
overlay.focus = None;
|
||||
cx.notify();
|
||||
}
|
||||
}))
|
||||
.child(
|
||||
Icon::new(IconName::ChevronLeft)
|
||||
.small()
|
||||
.text_color(cx.theme().muted_foreground),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.font_family(self.font_family.clone())
|
||||
.child(name),
|
||||
),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when(
|
||||
@@ -438,21 +445,23 @@ impl Tty7App {
|
||||
)
|
||||
.child(div().flex_1())
|
||||
.child(
|
||||
crate::ui::tab_strip::chrome_tile_sized(
|
||||
// Explicit tile, not `.small()`: this bar stands in for the
|
||||
// title bar while the overlay is up, so its close control is
|
||||
// the same tile the title bar's controls are.
|
||||
Button::new("diff-overlay-close").icon(Icon::new(IconName::Close)),
|
||||
crate::ui::app::TILE_SIZE,
|
||||
crate::ui::app::TILE_GLYPH_LINE,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
.rounded_lg()
|
||||
.tooltip("Close Diff (Esc)")
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.close_diff_overlay(window, cx);
|
||||
})),
|
||||
div().occlude().flex_shrink_0().child(
|
||||
crate::ui::tab_strip::chrome_tile_sized(
|
||||
// Explicit tile, not `.small()`: this bar stands in for the
|
||||
// title bar while the overlay is up, so its close control is
|
||||
// the same tile the title bar's controls are.
|
||||
Button::new("diff-overlay-close").icon(Icon::new(IconName::Close)),
|
||||
crate::ui::app::TILE_SIZE,
|
||||
crate::ui::app::TILE_GLYPH_LINE,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
.rounded_lg()
|
||||
.tooltip("Close Diff (Esc)")
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.close_diff_overlay(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+34
-34
@@ -13,16 +13,13 @@
|
||||
|
||||
use gpui::{
|
||||
Animation, AnimationExt as _, AnyElement, Axis, Bounds, Context, Div, FontWeight, MouseButton,
|
||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, SharedString, Stateful, Window,
|
||||
WindowControlArea, canvas, deferred, div, ease_out_quint, linear_color_stop, linear_gradient,
|
||||
prelude::*, px,
|
||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, SharedString, Stateful, Window, canvas,
|
||||
deferred, div, ease_out_quint, linear_color_stop, linear_gradient, prelude::*, px,
|
||||
};
|
||||
use gpui_component::button::{Button, ButtonVariants as _};
|
||||
use gpui_component::input::Input;
|
||||
use gpui_component::menu::{ContextMenu, ContextMenuExt as _};
|
||||
use gpui_component::{
|
||||
ActiveTheme as _, Icon, IconName, InteractiveElementExt as _, Sizable as _, h_flex, v_flex,
|
||||
};
|
||||
use gpui_component::{ActiveTheme as _, Icon, IconName, Sizable as _, h_flex, v_flex};
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::rc::Rc;
|
||||
|
||||
@@ -751,11 +748,35 @@ impl Tty7App {
|
||||
let controls = h_flex()
|
||||
.flex_shrink_0()
|
||||
.h(px(TITLE_BAR_HEIGHT))
|
||||
// Same box as the real title bar this row stands in for, hairline
|
||||
// included: gpui-component's `TitleBar` draws a `border_b_1` inside its
|
||||
// own `TITLE_BAR_HEIGHT` (tty7 paints it transparent, but it still takes
|
||||
// its pixel), so the bar centres its contents on 19.5 while an
|
||||
// unbordered 40px row centres them on 20. Half a pixel is invisible on
|
||||
// the line-art tiles, and *not* on the solid brand mark: collapsing the
|
||||
// rail hands the mark from this row to the bar, and it visibly hopped up
|
||||
// as it went. Reserve the same pixel here and the handover is still.
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().transparent)
|
||||
.items_center()
|
||||
.justify_end()
|
||||
.gap(px(2.))
|
||||
// Glyph's ink, not hit box, on the content edge — see `TILE_PAD`.
|
||||
.pr(px(crate::ui::app::tile_trailing_inset()))
|
||||
// The brand mark leads the row, on the rail's own content inset — the
|
||||
// line the search magnifier and every row label below it start on, so
|
||||
// it reads as the head of this column rather than a floating badge.
|
||||
// The spacer is what keeps the controls pinned right once the row has
|
||||
// a leading child (`justify_end` alone no longer does it).
|
||||
.when_some(crate::ui::app::window_mark(), |row, mark| {
|
||||
row.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.pl(px(crate::ui::app::CONTENT_INSET))
|
||||
.child(mark),
|
||||
)
|
||||
.child(div().flex_1())
|
||||
})
|
||||
// Both tiles are wrapped in an `occlude()` div, exactly like the
|
||||
// title-strip chrome. This row is a `WindowControlArea::Drag` (set
|
||||
// below), which on Windows maps to HTCAPTION — the OS claims the click
|
||||
@@ -934,34 +955,13 @@ impl Tty7App {
|
||||
//
|
||||
// The real `TitleBar` — which carries the window's drag region
|
||||
// — only spans the *right* column in this layout, so this strip
|
||||
// would be dead space you can't grab the window by. Make the
|
||||
// controls' own row act like the title bar it sits level with:
|
||||
// drag to move, double-click to zoom. Driven exactly like
|
||||
// `TitleBar` does it (and the settings overlay's stand-in
|
||||
// strip): a press arms a flag and the first *move* starts the
|
||||
// window move, so a plain click — and a double-click — still
|
||||
// lands intact, while the buttons on the right keep taking
|
||||
// their own clicks.
|
||||
.child({
|
||||
let should_move = Rc::new(Cell::new(false));
|
||||
controls
|
||||
.id("sidebar-titlebar-drag")
|
||||
.window_control_area(WindowControlArea::Drag)
|
||||
.on_mouse_down(MouseButton::Left, {
|
||||
let should_move = should_move.clone();
|
||||
move |_, _, _| should_move.set(true)
|
||||
})
|
||||
.on_mouse_up(MouseButton::Left, {
|
||||
let should_move = should_move.clone();
|
||||
move |_, _, _| should_move.set(false)
|
||||
})
|
||||
.on_mouse_move(move |_, window, _| {
|
||||
if should_move.replace(false) {
|
||||
window.start_window_move();
|
||||
}
|
||||
})
|
||||
.on_double_click(|_, window, _| window.titlebar_double_click())
|
||||
})
|
||||
// would be dead space you can't grab the window by. `title_bar_drag`
|
||||
// makes the controls' row act like the bar it sits level with:
|
||||
// drag to move, double-click to zoom, while the buttons on the
|
||||
// right keep taking their own clicks (they're `occlude()`d).
|
||||
.child(crate::ui::app::title_bar_drag(
|
||||
controls.id("sidebar-titlebar-drag"),
|
||||
))
|
||||
.child(top_bar)
|
||||
.child(crate::ui::scrollbar::with_vertical_scrollbar(
|
||||
"tab-sidebar-scrollbar",
|
||||
|
||||
@@ -1518,6 +1518,25 @@ impl Tty7App {
|
||||
// Negative off macOS only: the bar already inset us past the window
|
||||
// controls, and there the reserve *is* the clearance.
|
||||
.ml(px(crate::ui::app::title_bar_hug_offset()))
|
||||
// The brand mark follows the rail's controls into the strip, so
|
||||
// collapsing the sidebar doesn't strip the window's leading corner
|
||||
// back to nothing (see `app::window_mark`). The group is anchored by
|
||||
// its tiles' *hit boxes*, which start `tile_trailing_inset()` from
|
||||
// the window edge; the mark has no box, so it adds the difference
|
||||
// back to land its own ink on `CONTENT_INSET` like the rail's did.
|
||||
.when_some(crate::ui::app::window_mark(), |group, mark| {
|
||||
group.child(
|
||||
div()
|
||||
.flex_shrink_0()
|
||||
.pl(px(crate::ui::app::CONTENT_INSET
|
||||
- crate::ui::app::tile_trailing_inset()))
|
||||
// The mark is solid where the tiles are line work, so it
|
||||
// needs more air than the 2px that separates two tiles
|
||||
// before the "+" beside it stops reading as part of it.
|
||||
.pr(px(4.))
|
||||
.child(mark),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div().occlude().flex_shrink_0().child(
|
||||
self.attach_new_tab_menu(
|
||||
|
||||
Reference in New Issue
Block a user