Merge top bar + sidebar into one connected chrome shell (L-frame)
The top bar and left sidebar were two separate rounded panels with a 12px gap between them; the user wants them to read as one continuous shell with a larger outer radius. - Add per-corner radii to the UI quad pipeline (UiQuad::with_corners, a new vertex attribute at location 6, and a per-quadrant radius select in the fragment shader's sdRoundedBox). Existing call sites default to the uniform radius, so nothing else changes. - Close the 12px gap: the sidebar now abuts the top bar's bottom edge and runs to the window's bottom margin, sharing the left edge. The join corners are squared while the outer corners take a new, larger UI_SHELL_RADIUS_LOGICAL (14) so the whole chrome reads as one card. - Smoothly close the join corner as the sidebar slides in, instead of popping from round to square when the panel becomes visible. - Seal the seam: the sidebar (same color, drawn on top) extends up a couple physical px under the top bar so the two translucent edges' antialiasing can't leak the dark background as a hairline "black edge".
This commit is contained in:
1 parent
6b92a7a927
commit
510010fb3f
5 files changed
+138
-48
No files matched your search
@@ -13,6 +13,7 @@ varying mediump vec2 quadSize;
|
||||
varying mediump float quadRadius;
|
||||
varying mediump float quadFeather;
|
||||
varying mediump vec2 grad;
|
||||
varying mediump vec4 corners;
|
||||
varying mediump vec4 color0;
|
||||
varying mediump vec4 color1;
|
||||
#else
|
||||
@@ -26,14 +27,21 @@ in vec2 quadSize;
|
||||
in float quadRadius;
|
||||
in float quadFeather;
|
||||
in vec2 grad;
|
||||
in vec4 corners;
|
||||
in vec4 color0;
|
||||
in vec4 color1;
|
||||
#endif
|
||||
|
||||
// Signed distance to a rounded box centered at the origin.
|
||||
float_t sdRoundedBox(vec2 p, vec2 halfSize, float_t radius) {
|
||||
vec2 q = abs(p) - halfSize + radius;
|
||||
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - radius;
|
||||
// Signed distance to a rounded box centered at the origin, with per-corner
|
||||
// radii. `radii` = [top-left, top-right, bottom-right, bottom-left]; the corner
|
||||
// facing `p`'s quadrant is selected so join corners can stay square while the
|
||||
// outer ones curve (the connected top-bar/sidebar L-frame).
|
||||
float_t sdRoundedBox(vec2 p, vec2 halfSize, vec4 radii) {
|
||||
// p.x > 0 -> right half (top-right / bottom-right), else left half.
|
||||
float_t r = (p.x > 0.0) ? ((p.y > 0.0) ? radii.z : radii.y)
|
||||
: ((p.y > 0.0) ? radii.w : radii.x);
|
||||
vec2 q = abs(p) - halfSize + r;
|
||||
return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r;
|
||||
}
|
||||
|
||||
void main() {
|
||||
@@ -54,8 +62,11 @@ void main() {
|
||||
coverage = 1.0;
|
||||
} else {
|
||||
// Crisp rounded box with 1px antialiasing from the signed distance.
|
||||
float_t radius = min(quadRadius, min(halfSize.x, halfSize.y));
|
||||
float_t d = sdRoundedBox(p, halfSize, radius);
|
||||
// Each corner is clamped independently so a large outer radius and a
|
||||
// square (0) join corner can coexist on the same quad.
|
||||
float_t lim = min(halfSize.x, halfSize.y);
|
||||
vec4 radii = min(corners, vec4(lim));
|
||||
float_t d = sdRoundedBox(p, halfSize, radii);
|
||||
coverage = clamp(0.5 - d, 0.0, 1.0);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ attribute vec2 aPos;
|
||||
attribute vec2 aUv;
|
||||
attribute vec4 aSizeRadius;
|
||||
attribute vec2 aGrad;
|
||||
attribute vec4 aCorners;
|
||||
attribute vec4 aColor0;
|
||||
attribute vec4 aColor1;
|
||||
|
||||
@@ -14,6 +15,7 @@ varying mediump vec2 quadSize;
|
||||
varying mediump float quadRadius;
|
||||
varying mediump float quadFeather;
|
||||
varying mediump vec2 grad;
|
||||
varying mediump vec4 corners;
|
||||
varying mediump vec4 color0;
|
||||
varying mediump vec4 color1;
|
||||
#else
|
||||
@@ -21,6 +23,7 @@ layout(location = 0) in vec2 aPos;
|
||||
layout(location = 1) in vec2 aUv;
|
||||
layout(location = 2) in vec4 aSizeRadius;
|
||||
layout(location = 3) in vec2 aGrad;
|
||||
layout(location = 6) in vec4 aCorners;
|
||||
layout(location = 4) in vec4 aColor0;
|
||||
layout(location = 5) in vec4 aColor1;
|
||||
|
||||
@@ -29,6 +32,7 @@ out vec2 quadSize;
|
||||
out float quadRadius;
|
||||
out float quadFeather;
|
||||
out vec2 grad;
|
||||
out vec4 corners;
|
||||
out vec4 color0;
|
||||
out vec4 color1;
|
||||
#endif
|
||||
@@ -39,6 +43,7 @@ void main() {
|
||||
quadRadius = aSizeRadius.z;
|
||||
quadFeather = aSizeRadius.w;
|
||||
grad = aGrad;
|
||||
corners = aCorners;
|
||||
color0 = aColor0;
|
||||
color1 = aColor1;
|
||||
gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0);
|
||||
|
||||
@@ -213,16 +213,20 @@ pub(super) fn chrome_tab_layout(
|
||||
};
|
||||
}
|
||||
|
||||
// Expanded panel: fills the left gutter between the top and bottom bars,
|
||||
// leaving a small breathing gap before the terminal grid on its right.
|
||||
// Expanded panel: the left leg of the connected chrome L-frame. It shares
|
||||
// the top bar's left edge (`margin`) and abuts its bottom edge with NO gap
|
||||
// (`panel_top = top + bar_h`) so the two read as one panel; the join corners
|
||||
// are squared off in `draw_chrome`. It runs down to the window's bottom
|
||||
// margin (symmetric with the top bar's top at `margin`), leaving only a
|
||||
// breathing gap before the terminal grid on its right.
|
||||
let sw = SIDEBAR_W_LOGICAL * scale_factor;
|
||||
let t = expand.clamp(0.0, 1.0);
|
||||
let eased = 1.0 - (1.0 - t) * (1.0 - t) * (1.0 - t);
|
||||
let slide = (1.0 - eased) * sw;
|
||||
let panel_x = margin - slide;
|
||||
let panel_w = (sw - margin - s(12.0)).max(s(120.0));
|
||||
let panel_top = top + bar_h + s(12.0);
|
||||
let panel_bottom = h - margin - s(12.0);
|
||||
let panel_top = top + bar_h;
|
||||
let panel_bottom = h - margin;
|
||||
let panel_h = (panel_bottom - panel_top).max(0.0);
|
||||
let panel = (panel_x, panel_top, panel_w, panel_h);
|
||||
|
||||
@@ -234,7 +238,10 @@ pub(super) fn chrome_tab_layout(
|
||||
let tab_w = panel_w - 2.0 * tab_pad;
|
||||
let row_h = s(34.0);
|
||||
let gap = s(8.0);
|
||||
let header = s(42.0); // room for a "TABS" caption at the panel top
|
||||
// Header band below the top-bar join: the panel now abuts the top bar with
|
||||
// no gap, so this carries the seam clearance (the old +12 panel gap) plus
|
||||
// room for the "TABS" caption and the "+" square.
|
||||
let header = s(54.0);
|
||||
|
||||
// "+" square, vertically centred in the header band, pinned to the right.
|
||||
let plus_sz = s(20.0);
|
||||
@@ -399,6 +406,9 @@ pub(super) fn draw_chrome(d: &mut Display) {
|
||||
let bar_h = s(40.0);
|
||||
let inner_pad = s(6.0);
|
||||
let radius = s(UI_CORNER_RADIUS_LOGICAL);
|
||||
// Larger outer radius for the connected chrome shell (top bar + sidebar
|
||||
// L-frame); pills/controls keep the smaller `radius`.
|
||||
let shell_r = s(UI_SHELL_RADIUS_LOGICAL);
|
||||
let pill_h = bar_h - 2.0 * inner_pad;
|
||||
let pill_r = s(UI_CORNER_RADIUS_LOGICAL);
|
||||
let hairline_w = s(UI_HAIRLINE_LOGICAL).max(1.0);
|
||||
@@ -430,28 +440,26 @@ pub(super) fn draw_chrome(d: &mut Display) {
|
||||
));
|
||||
}
|
||||
|
||||
// ---- Window border: same as the window background ----
|
||||
// The border is painted in the actual background color (custom override
|
||||
// or the scheme background) so the window edge reads as one cohesive
|
||||
// surface — no bright accent outline floating around the chrome.
|
||||
let bg = d.nebula_background.unwrap_or_else(|| d.colors[NamedColor::Background]);
|
||||
let border = Rgba::opaque(bg);
|
||||
let glow_b = s(8.0);
|
||||
quads.push(UiQuad::solid(0.0, 0.0, w, glow_b, 0.0, border));
|
||||
quads.push(UiQuad::solid(0.0, h - glow_b, w, glow_b, 0.0, border));
|
||||
quads.push(UiQuad::solid(0.0, 0.0, glow_b, h, 0.0, border));
|
||||
quads.push(UiQuad::solid(w - glow_b, 0.0, glow_b, h, 0.0, border));
|
||||
let t_b = s(1.5);
|
||||
quads.push(UiQuad::solid(0.0, 0.0, w, t_b, 0.0, border));
|
||||
quads.push(UiQuad::solid(0.0, h - t_b, w, t_b, 0.0, border));
|
||||
quads.push(UiQuad::solid(0.0, 0.0, t_b, h, 0.0, border));
|
||||
quads.push(UiQuad::solid(w - t_b, 0.0, t_b, h, 0.0, border));
|
||||
|
||||
// ---- Top title / tab bar ----
|
||||
let top_y = margin;
|
||||
quads.push(UiQuad::solid(margin, top_y, w - 2.0 * margin, bar_h, radius, palette.panel));
|
||||
|
||||
// The top bar and the left sidebar form one connected chrome shell (an
|
||||
// L-frame): the top bar's bottom-left corner is squared so the sidebar
|
||||
// abuts it seamlessly, while the three outer corners keep the larger shell
|
||||
// radius. Collapsed (no sidebar) the bar is a standalone card, all four
|
||||
// corners shell-rounded.
|
||||
let shell_r = s(UI_SHELL_RADIUS_LOGICAL);
|
||||
let sidebar_expand = d.left_sidebar_progress();
|
||||
let top_y = margin;
|
||||
// The bottom-left corner closes toward square as the sidebar slides in, so
|
||||
// the join seals smoothly instead of the corner popping from round to square
|
||||
// the instant the panel becomes visible. `expand` is 0 collapsed → 1 open.
|
||||
let join_r = shell_r * (1.0 - sidebar_expand.clamp(0.0, 1.0));
|
||||
// [top-left, top-right, bottom-right, bottom-left]
|
||||
let top_bar_corners = [shell_r, shell_r, shell_r, join_r];
|
||||
quads.push(
|
||||
UiQuad::solid(margin, top_y, w - 2.0 * margin, bar_h, radius, palette.panel)
|
||||
.with_corners(top_bar_corners),
|
||||
);
|
||||
|
||||
let tab_layout = chrome_tab_layout(
|
||||
&size,
|
||||
scale,
|
||||
@@ -499,10 +507,27 @@ pub(super) fn draw_chrome(d: &mut Display) {
|
||||
quads.push(UiQuad::solid(set_x, set_y, set_w, set_h, pill_r, HOVER_FILL_STRONG));
|
||||
}
|
||||
|
||||
// Sidebar panel background (draws through slide-out so folding is visible).
|
||||
// Sidebar panel background — the left leg of the chrome shell. Its top
|
||||
// corners are squared so it abuts the top bar's squared bottom-left into
|
||||
// one continuous L; only the two outer bottom corners keep the shell
|
||||
// radius. Draws through slide-out so folding stays visible.
|
||||
//
|
||||
// Seam seal: the sidebar is painted AFTER the top bar and in the same
|
||||
// panel color, so we extend its top edge UP by a couple of physical pixels
|
||||
// to tuck under the top bar. Without this, the top bar's bottom edge and
|
||||
// the sidebar's top edge each carry a 1px antialiasing falloff; where two
|
||||
// translucent edges merely abut, both are under-covered and the dark
|
||||
// background bleeds through as a hairline "black edge". Overlapping turns
|
||||
// that transition into panel-over-panel (≈1.5% delta, invisible) and the
|
||||
// gap is gone.
|
||||
if d.left_sidebar_visible() {
|
||||
let (pnl_x, pnl_y, pnl_w, pnl_h) = tab_layout.panel;
|
||||
quads.push(UiQuad::solid(pnl_x, pnl_y, pnl_w, pnl_h, radius, palette.panel));
|
||||
let seam = s(2.0).max(1.0);
|
||||
quads.push(
|
||||
UiQuad::solid(pnl_x, pnl_y - seam, pnl_w, pnl_h + seam, radius, palette.panel)
|
||||
// [top-left, top-right, bottom-right, bottom-left]
|
||||
.with_corners([0.0, 0.0, shell_r, shell_r]),
|
||||
);
|
||||
}
|
||||
|
||||
// Dock preview: while a dragged tab hovers the terminal area, glow the
|
||||
@@ -576,17 +601,24 @@ pub(super) fn draw_chrome(d: &mut Display) {
|
||||
Rgba::new(accent.r, accent.g, accent.b, 40),
|
||||
));
|
||||
quads.push(UiQuad::solid(tab_draw_x, tab_draw_y, tab_w, tab_h, pill_r, palette.tab_bg_l));
|
||||
quads.push(UiQuad::solid(
|
||||
tab_draw_x,
|
||||
tab_draw_y,
|
||||
tab_w,
|
||||
tab_h,
|
||||
pill_r,
|
||||
Rgba::new(accent.r, accent.g, accent.b, 26),
|
||||
));
|
||||
} else {
|
||||
quads.push(UiQuad::solid(tab_draw_x, tab_draw_y, tab_w, tab_h, pill_r, palette.pill));
|
||||
// The accent wash on top is a DARK-theme depth cue: the white pill
|
||||
// (`tab_bg_l`) is the state on the light themes, so tinting it just
|
||||
// grays the white the user asked to keep pure. Only the dark themes
|
||||
// layer the wash over their (dark) active pill.
|
||||
if !palette.is_light {
|
||||
quads.push(UiQuad::solid(
|
||||
tab_draw_x,
|
||||
tab_draw_y,
|
||||
tab_w,
|
||||
tab_h,
|
||||
pill_r,
|
||||
Rgba::new(accent.r, accent.g, accent.b, 26),
|
||||
));
|
||||
}
|
||||
}
|
||||
// Inactive tabs carry no standalone fill — they sit flush on the
|
||||
// sidebar surface and only light up on hover (below). State is the
|
||||
// white active pill, not a per-row background.
|
||||
|
||||
if tab_hovered {
|
||||
quads.push(UiQuad::solid(tab_draw_x, tab_draw_y, tab_w, tab_h, pill_r, HOVER_FILL));
|
||||
@@ -670,9 +702,10 @@ pub(super) fn draw_chrome(d: &mut Display) {
|
||||
}
|
||||
|
||||
// "New tab" pill (a wide row when expanded, a square beside the toggle
|
||||
// when collapsed — both come straight from the layout).
|
||||
// when collapsed — both come straight from the layout). No resting fill:
|
||||
// the "+" is just an icon until the pointer arrives, then a hover pill
|
||||
// lifts under it (matches the top-bar window controls).
|
||||
let (plus_x, plus_y, plus_w, plus_h) = tab_layout.plus;
|
||||
quads.push(UiQuad::solid(plus_x, plus_y, plus_w, plus_h, pill_r, palette.pill));
|
||||
if d.nebula_chrome_hover == ChromeHit::NewTab {
|
||||
quads.push(UiQuad::solid(plus_x, plus_y, plus_w, plus_h, pill_r, HOVER_FILL_STRONG));
|
||||
}
|
||||
@@ -863,12 +896,14 @@ pub(super) fn draw_chrome(d: &mut Display) {
|
||||
// Sidebar text (caption, labels, × buttons) also stays under the
|
||||
// settings glass — skip it entirely while the modal is up.
|
||||
if d.left_sidebar_visible() && tab_layout.panel.2 > 0.0 && !d.nebula_settings_open {
|
||||
// "TABS" caption at the panel head.
|
||||
// "TABS" caption at the panel head. The panel now abuts the top bar
|
||||
// with no gap, so the caption is pushed down inside the header band to
|
||||
// keep clearance from the (invisible) join seam.
|
||||
let (pnl_x, pnl_y, _, _) = tab_layout.panel;
|
||||
d.renderer.draw_chrome_text(
|
||||
&size,
|
||||
pnl_x + s(16.0),
|
||||
pnl_y + s(11.0),
|
||||
pnl_y + s(18.0),
|
||||
TXT_DIM,
|
||||
"TABS",
|
||||
&mut d.glyph_cache,
|
||||
|
||||
@@ -152,9 +152,17 @@ pub(crate) const NEBULA_UNFOCUSED_SPLIT_DIM: f32 = 0.30;
|
||||
/// bar while the symmetric `SizeInfo` padding model is still in use.
|
||||
pub const CHROME_BAR_LOGICAL: f32 = 56.0;
|
||||
|
||||
/// Shared chrome/control corner radius.
|
||||
/// Shared chrome/control corner radius. Used for the small in-shell affordances
|
||||
/// (window-control hover pills, tab pills, the "+" square) — kept modest so the
|
||||
/// controls stay crisp.
|
||||
pub(super) const UI_CORNER_RADIUS_LOGICAL: f32 = 8.0;
|
||||
|
||||
/// Outer radius of the connected chrome shell (the L-frame formed by the top
|
||||
/// bar + left sidebar). Larger than the control radius so the whole window
|
||||
/// chrome reads as one soft-cornered card while the affordances inside keep
|
||||
/// their tighter [`UI_CORNER_RADIUS_LOGICAL`] curve.
|
||||
pub(super) const UI_SHELL_RADIUS_LOGICAL: f32 = 14.0;
|
||||
|
||||
/// Shared quiet outline thickness.
|
||||
pub(super) const UI_HAIRLINE_LOGICAL: f32 = 1.0;
|
||||
|
||||
|
||||
@@ -80,6 +80,12 @@ pub struct UiQuad {
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
pub radius: f32,
|
||||
/// Per-corner radii in pixels `[top-left, top-right, bottom-right,
|
||||
/// bottom-left]`. Defaults to `[radius; 4]`; set via [`UiQuad::with_corners`]
|
||||
/// to round only some corners (e.g. the connected top-bar/sidebar L-frame,
|
||||
/// where the join corners are square and only the outer corners curve).
|
||||
/// `radius` still drives the flat/glow shader flags, so keep it ≥ 0 here.
|
||||
pub corner_radii: [f32; 4],
|
||||
/// Soft glow falloff in pixels; `0.0` means a crisp rounded rectangle.
|
||||
pub feather: f32,
|
||||
/// Explicit corner positions in pixels `[top-left, bottom-left, top-right,
|
||||
@@ -107,6 +113,7 @@ impl UiQuad {
|
||||
width,
|
||||
height,
|
||||
radius,
|
||||
corner_radii: [radius; 4],
|
||||
feather: 0.0,
|
||||
corners: None,
|
||||
v_range: [0.0, 1.0],
|
||||
@@ -134,6 +141,7 @@ impl UiQuad {
|
||||
width,
|
||||
height,
|
||||
radius,
|
||||
corner_radii: [radius; 4],
|
||||
feather: 0.0,
|
||||
corners: None,
|
||||
v_range: [0.0, 1.0],
|
||||
@@ -152,6 +160,7 @@ impl UiQuad {
|
||||
width,
|
||||
height,
|
||||
radius: 0.0,
|
||||
corner_radii: [0.0; 4],
|
||||
feather: 1.0,
|
||||
corners: None,
|
||||
v_range: [0.0, 1.0],
|
||||
@@ -176,6 +185,7 @@ impl UiQuad {
|
||||
width: 0.0,
|
||||
height: 0.0,
|
||||
radius: -1.0,
|
||||
corner_radii: [0.0; 4],
|
||||
feather: 0.0,
|
||||
corners: Some(corners),
|
||||
v_range: [0.0, 1.0],
|
||||
@@ -185,6 +195,19 @@ impl UiQuad {
|
||||
}
|
||||
}
|
||||
|
||||
/// Rounded rectangle with independent per-corner radii, ordered
|
||||
/// `[top-left, top-right, bottom-right, bottom-left]`. Used for the
|
||||
/// connected top-bar / sidebar L-frame, where the two join corners stay
|
||||
/// square (radius 0) and only the outer corners curve. `radius` is kept as
|
||||
/// the max of the four so the shader's flat/glow branch and the geometry's
|
||||
/// AA padding still behave; the fragment shader picks the right corner.
|
||||
#[inline]
|
||||
pub fn with_corners(mut self, corner_radii: [f32; 4]) -> Self {
|
||||
self.corner_radii = corner_radii;
|
||||
self.radius = corner_radii.iter().copied().fold(0.0_f32, f32::max);
|
||||
self
|
||||
}
|
||||
|
||||
/// Clip the quad to the vertical band `top..bot` (screen px). The visible
|
||||
/// part renders pixel-identical to the unclipped quad — rounded corners,
|
||||
/// gradients and glows are cut mid-shape rather than re-rounded — because
|
||||
@@ -226,6 +249,8 @@ struct UiVertex {
|
||||
// Gradient axis in uv space.
|
||||
gx: f32,
|
||||
gy: f32,
|
||||
// Per-corner radii in pixels: [top-left, top-right, bottom-right, bottom-left].
|
||||
corners: [f32; 4],
|
||||
// Endpoint colors.
|
||||
c0: [u8; 4],
|
||||
c1: [u8; 4],
|
||||
@@ -276,6 +301,11 @@ impl UiRenderer {
|
||||
gl::EnableVertexAttribArray(3);
|
||||
offset += (mem::size_of::<f32>() * 2) as i32;
|
||||
|
||||
// aCorners (location 6): vec4 per-corner radii (TL, TR, BR, BL).
|
||||
gl::VertexAttribPointer(6, 4, gl::FLOAT, gl::FALSE, stride, offset as *const _);
|
||||
gl::EnableVertexAttribArray(6);
|
||||
offset += (mem::size_of::<f32>() * 4) as i32;
|
||||
|
||||
// aColor0 (location 4): normalized ubyte4.
|
||||
gl::VertexAttribPointer(4, 4, gl::UNSIGNED_BYTE, gl::TRUE, stride, offset as *const _);
|
||||
gl::EnableVertexAttribArray(4);
|
||||
@@ -353,6 +383,7 @@ impl UiRenderer {
|
||||
feather: quad.feather,
|
||||
gx,
|
||||
gy,
|
||||
corners: quad.corner_radii,
|
||||
c0,
|
||||
c1,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user