1
0
Fork 0
forked from wry/wry

tabs: hy3 tab styling, with corresponding config options.

tab-bar-height = 22
tab-bar-padding = 5
tab-bar-radius = 6
tab-bar-border-width = 2
tab-bar-text-padding = 3
tab-bar-gap = 6
tab-title-align = "center"
tab-active-bg-color = "#33ccff40"
tab-active-border-color = "#33ccffee"
tab-active-text-color = "#ffffffff"
tab-focused-bg-color = "#60606040"
tab-focused-border-color = "#808080ee"
tab-focused-text-color = "#ffffffff"
tab-inactive-bg-color = "#30303020"
tab-inactive-border-color = "#606060aa"
tab-inactive-text-color = "#ffffffff"
tab-urgent-bg-color = "#ff223340"
tab-urgent-border-color = "#ff2233ee"
tab-urgent-text-color = "#ffffffff"
This commit is contained in:
entailz 2026-05-29 03:16:22 -07:00
parent e35dce433a
commit e8f86dae8a
28 changed files with 920 additions and 242 deletions

View file

@ -2055,6 +2055,7 @@ impl ConfigClient {
radius radius
} }
#[allow(dead_code)]
pub fn seat_toggle_expand(&self, seat: Seat) { pub fn seat_toggle_expand(&self, seat: Seat) {
self.send(&ClientMessage::SeatToggleExpand { seat }); self.send(&ClientMessage::SeatToggleExpand { seat });
} }
@ -2087,6 +2088,14 @@ impl ConfigClient {
self.send(&ClientMessage::SetTabTitleAlign { align }); self.send(&ClientMessage::SetTabTitleAlign { align });
} }
pub fn set_tab_from_top(&self, from_top: bool) {
self.send(&ClientMessage::SetTabFromTop { from_top });
}
pub fn set_tab_render_text(&self, render: bool) {
self.send(&ClientMessage::SetTabRenderText { render });
}
pub fn seat_move_tab(&self, seat: Seat, right: bool) { pub fn seat_move_tab(&self, seat: Seat, right: bool) {
self.send(&ClientMessage::SeatMoveTab { seat, right }); self.send(&ClientMessage::SeatMoveTab { seat, right });
} }

View file

@ -911,6 +911,12 @@ pub enum ClientMessage<'a> {
SetTabTitleAlign { SetTabTitleAlign {
align: u32, align: u32,
}, },
SetTabFromTop {
from_top: bool,
},
SetTabRenderText {
render: bool,
},
SeatMoveTab { SeatMoveTab {
seat: Seat, seat: Seat,
right: bool, right: bool,

View file

@ -464,6 +464,21 @@ pub fn set_tab_title_align(align: &str) {
get!().set_tab_title_align(val) get!().set_tab_title_align(val)
} }
/// Sets whether tab enter/exit animations slide from the top.
///
/// When `true`, tabs slide in from above and out upwards.
/// When `false` (default), tabs slide in from below and out downwards.
pub fn set_tab_from_top(from_top: bool) {
get!().set_tab_from_top(from_top)
}
/// Sets whether text is rendered on tab buttons.
///
/// The default is `true`.
pub fn set_tab_render_text(render: bool) {
get!().set_tab_render_text(render)
}
/// Sets a callback to run when this config is unloaded. /// Sets a callback to run when this config is unloaded.
/// ///
/// Only one callback can be set at a time. If another callback is already set, it will be /// Only one callback can be set at a time. If another callback is already set, it will be

View file

@ -334,6 +334,30 @@ pub mod colors {
/// ///
/// Default: `#23092c`. /// Default: `#23092c`.
const 23 => TAB_ATTENTION_BACKGROUND_COLOR, const 23 => TAB_ATTENTION_BACKGROUND_COLOR,
/// The background color of a focused (keyboard-focused) tab.
///
/// Default: `#3a5a70`.
const 24 => TAB_FOCUSED_BACKGROUND_COLOR,
/// The border color of a focused (keyboard-focused) tab.
///
/// Default: `#285577`.
const 25 => TAB_FOCUSED_BORDER_COLOR,
/// The text color of a focused (keyboard-focused) tab.
///
/// Default: `#dddddd`.
const 26 => TAB_FOCUSED_TEXT_COLOR,
/// The background color of an urgent tab.
///
/// Default: `#23092c`.
const 27 => TAB_URGENT_BACKGROUND_COLOR,
/// The border color of an urgent tab.
///
/// Default: `#441155`.
const 28 => TAB_URGENT_BORDER_COLOR,
/// The text color of an urgent tab.
///
/// Default: `#ffffff`.
const 29 => TAB_URGENT_TEXT_COLOR,
} }
/// Sets the color of GUI element. /// Sets the color of GUI element.
@ -430,5 +454,9 @@ pub mod sized {
/// ///
/// Default: 4 /// Default: 4
const 12 => TAB_BAR_GAP, const 12 => TAB_BAR_GAP,
/// The opacity of tabs in the tab bar (0-100).
///
/// Default: 100
const 13 => TAB_OPACITY,
} }
} }

View file

@ -399,10 +399,10 @@ fn start_compositor2(
hyprland_global_shortcuts: Default::default(), hyprland_global_shortcuts: Default::default(),
layer_rules: Default::default(), layer_rules: Default::default(),
blur_config: Default::default(), blur_config: Default::default(),
blur_cache_epoch: Default::default(),
animations_config: Default::default(), animations_config: Default::default(),
active_animations: Default::default(), active_animations: Default::default(),
close_snapshots: Default::default(), close_snapshots: Default::default(),
tab_animation_containers: Default::default(),
}); });
state.tracker.register(ClientId::from_raw(0)); state.tracker.register(ClientId::from_raw(0));
create_dummy_output(&state); create_dummy_output(&state);

View file

@ -2505,6 +2505,7 @@ impl ConfigProxyHandler {
TAB_BAR_BORDER_WIDTH => ThemeSized::tab_bar_border_width, TAB_BAR_BORDER_WIDTH => ThemeSized::tab_bar_border_width,
TAB_BAR_TEXT_PADDING => ThemeSized::tab_bar_text_padding, TAB_BAR_TEXT_PADDING => ThemeSized::tab_bar_text_padding,
TAB_BAR_GAP => ThemeSized::tab_bar_gap, TAB_BAR_GAP => ThemeSized::tab_bar_gap,
TAB_OPACITY => ThemeSized::tab_opacity,
_ => return Err(CphError::UnknownSized(sized.0)), _ => return Err(CphError::UnknownSized(sized.0)),
}; };
Ok(sized) Ok(sized)
@ -2584,10 +2585,16 @@ impl ConfigProxyHandler {
HIGHLIGHT_COLOR => ThemeColor::highlight, HIGHLIGHT_COLOR => ThemeColor::highlight,
TAB_ACTIVE_BACKGROUND_COLOR => ThemeColor::tab_active_background, TAB_ACTIVE_BACKGROUND_COLOR => ThemeColor::tab_active_background,
TAB_ACTIVE_BORDER_COLOR => ThemeColor::tab_active_border, TAB_ACTIVE_BORDER_COLOR => ThemeColor::tab_active_border,
TAB_FOCUSED_BACKGROUND_COLOR => ThemeColor::tab_focused_background,
TAB_FOCUSED_BORDER_COLOR => ThemeColor::tab_focused_border,
TAB_INACTIVE_BACKGROUND_COLOR => ThemeColor::tab_inactive_background, TAB_INACTIVE_BACKGROUND_COLOR => ThemeColor::tab_inactive_background,
TAB_INACTIVE_BORDER_COLOR => ThemeColor::tab_inactive_border, TAB_INACTIVE_BORDER_COLOR => ThemeColor::tab_inactive_border,
TAB_URGENT_BACKGROUND_COLOR => ThemeColor::tab_urgent_background,
TAB_URGENT_BORDER_COLOR => ThemeColor::tab_urgent_border,
TAB_ACTIVE_TEXT_COLOR => ThemeColor::tab_active_text, TAB_ACTIVE_TEXT_COLOR => ThemeColor::tab_active_text,
TAB_FOCUSED_TEXT_COLOR => ThemeColor::tab_focused_text,
TAB_INACTIVE_TEXT_COLOR => ThemeColor::tab_inactive_text, TAB_INACTIVE_TEXT_COLOR => ThemeColor::tab_inactive_text,
TAB_URGENT_TEXT_COLOR => ThemeColor::tab_urgent_text,
TAB_BAR_BACKGROUND_COLOR => ThemeColor::tab_bar_background, TAB_BAR_BACKGROUND_COLOR => ThemeColor::tab_bar_background,
TAB_ATTENTION_BACKGROUND_COLOR => ThemeColor::tab_attention_background, TAB_ATTENTION_BACKGROUND_COLOR => ThemeColor::tab_attention_background,
_ => return Err(CphError::UnknownColor(colorable.0)), _ => return Err(CphError::UnknownColor(colorable.0)),
@ -3525,6 +3532,12 @@ impl ConfigProxyHandler {
}; };
self.state.theme.tab_title_align.set(val); self.state.theme.tab_title_align.set(val);
} }
ClientMessage::SetTabFromTop { from_top } => {
self.state.theme.tab_from_top.set(from_top);
}
ClientMessage::SetTabRenderText { render } => {
self.state.theme.tab_render_text.set(render);
}
ClientMessage::SeatMoveTab { seat, right } => self ClientMessage::SeatMoveTab { seat, right } => self
.handle_seat_move_tab(seat, right) .handle_seat_move_tab(seat, right)
.wrn("seat_move_tab")?, .wrn("seat_move_tab")?,

View file

@ -250,7 +250,8 @@ impl CursorUserGroup {
} }
fn damage(&self, rect: Rect) { fn damage(&self, rect: Rect) {
self.state.damage2(true, self.hardware_cursor.get(), rect); self.state
.damage2(true, self.hardware_cursor.get(), None, rect);
} }
} }

View file

@ -116,6 +116,11 @@ pub struct BlurBackdrop {
pub cache: Option<Rc<std::cell::RefCell<Option<BlurCacheEntry>>>>, pub cache: Option<Rc<std::cell::RefCell<Option<BlurCacheEntry>>>>,
pub cache_epoch: u64, pub cache_epoch: u64,
pub cache_pixel_rect: [i32; 4], pub cache_pixel_rect: [i32; 4],
/// Corner radius in physical pixels for rounded-rect clipping of the blur.
/// 0.0 = no rounding.
pub corner_radius: f32,
/// Rect size in physical pixels (width, height), used for SDF rounding.
pub pixel_size: [f32; 2],
} }
impl std::fmt::Debug for BlurBackdrop { impl std::fmt::Debug for BlurBackdrop {
@ -363,16 +368,6 @@ pub struct RoundedFillRect {
pub z_order: u32, pub z_order: u32,
} }
impl RoundedFillRect {
pub fn effective_color(&self) -> Color {
let mut color = self.color;
if let Some(alpha) = self.alpha {
color = color * alpha;
}
color
}
}
pub struct RoundedCopyTexture { pub struct RoundedCopyTexture {
pub tex: Rc<dyn GfxTexture>, pub tex: Rc<dyn GfxTexture>,
pub source: SampleRect, pub source: SampleRect,

View file

@ -660,8 +660,8 @@ fn render_blur_backdrop(fb: &Framebuffer, b: &BlurBackdrop) {
.mask .mask
.as_ref() .as_ref()
.and_then(|m| m.texture.as_gl().map(|t| (m, t))); .and_then(|m| m.texture.as_gl().map(|t| (m, t)));
if let Some((mask, mask_tex_obj)) = mask_gl if b.corner_radius > 0.0
&& !mask_tex_obj.gl.external_only || matches!(mask_gl.as_ref(), Some((_, mask_tex_obj)) if !mask_tex_obj.gl.external_only)
{ {
// Masked composite: src = (blurred * weight, weight); blend = (ONE, ONE_MINUS_SRC_ALPHA). // Masked composite: src = (blurred * weight, weight); blend = (ONE, ONE_MINUS_SRC_ALPHA).
let prog = &ctx.blur_composite; let prog = &ctx.blur_composite;
@ -670,14 +670,38 @@ fn render_blur_backdrop(fb: &Framebuffer, b: &BlurBackdrop) {
(gles.glBindTexture)(GL_TEXTURE_2D, texs[0]); (gles.glBindTexture)(GL_TEXTURE_2D, texs[0]);
(gles.glUniform1i)(prog.tex, 0); (gles.glUniform1i)(prog.tex, 0);
(gles.glActiveTexture)(GL_TEXTURE1); (gles.glActiveTexture)(GL_TEXTURE1);
(gles.glBindTexture)(GL_TEXTURE_2D, mask_tex_obj.gl.tex); let has_mask = if let Some((_, mask_tex_obj)) = mask_gl.as_ref() {
if !mask_tex_obj.gl.external_only {
(gles.glBindTexture)(GL_TEXTURE_2D, mask_tex_obj.gl.tex);
true
} else {
(gles.glBindTexture)(GL_TEXTURE_2D, texs[0]);
false
}
} else {
(gles.glBindTexture)(GL_TEXTURE_2D, texs[0]);
false
};
(gles.glTexParameteri)(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); (gles.glTexParameteri)(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
(gles.glTexParameteri)(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); (gles.glTexParameteri)(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
(gles.glTexParameteri)(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); (gles.glTexParameteri)(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
(gles.glTexParameteri)(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); (gles.glTexParameteri)(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
(gles.glUniform1i)(prog.mask_tex, 1); (gles.glUniform1i)(prog.mask_tex, 1);
(gles.glUniform1f)(prog.threshold, mask.threshold); (gles.glUniform1f)(
let mask_tc = mask.source.to_points(); prog.threshold,
mask_gl
.as_ref()
.map(|(mask, _)| mask.threshold)
.unwrap_or(1.0),
);
(gles.glUniform1f)(prog.corner_radius, b.corner_radius);
(gles.glUniform2f)(prog.pixel_size, b.pixel_size[0], b.pixel_size[1]);
(gles.glUniform1f)(prog.mask_enabled, if has_mask { 1.0 } else { 0.0 });
let mask_tc = mask_gl
.as_ref()
.filter(|(_, mask_tex_obj)| !mask_tex_obj.gl.external_only)
.map(|(mask, _)| mask.source.to_points())
.unwrap_or(texcoord);
(gles.glVertexAttribPointer)( (gles.glVertexAttribPointer)(
prog.texcoord as _, prog.texcoord as _,
2, 2,

View file

@ -83,6 +83,9 @@ pub(crate) struct BlurCompositeProg {
pub(crate) tex: GLint, pub(crate) tex: GLint,
pub(crate) mask_tex: GLint, pub(crate) mask_tex: GLint,
pub(crate) threshold: GLint, pub(crate) threshold: GLint,
pub(crate) corner_radius: GLint,
pub(crate) pixel_size: GLint,
pub(crate) mask_enabled: GLint,
} }
pub(crate) struct RoundedFillProg { pub(crate) struct RoundedFillProg {
@ -260,6 +263,9 @@ impl GlRenderContext {
tex: prog.get_uniform_location(c"tex"), tex: prog.get_uniform_location(c"tex"),
mask_tex: prog.get_uniform_location(c"mask_tex"), mask_tex: prog.get_uniform_location(c"mask_tex"),
threshold: prog.get_uniform_location(c"threshold"), threshold: prog.get_uniform_location(c"threshold"),
corner_radius: prog.get_uniform_location(c"corner_radius"),
pixel_size: prog.get_uniform_location(c"pixel_size"),
mask_enabled: prog.get_uniform_location(c"mask_enabled"),
prog, prog,
} }
}; };

View file

@ -5,6 +5,20 @@ varying vec2 v_mask_texcoord;
uniform sampler2D tex; uniform sampler2D tex;
uniform sampler2D mask_tex; uniform sampler2D mask_tex;
uniform float threshold; uniform float threshold;
uniform float corner_radius;
uniform vec2 pixel_size;
uniform float mask_enabled;
float rounded_alpha(vec2 uv) {
if (corner_radius <= 0.0)
return 1.0;
float radius = min(corner_radius, min(pixel_size.x, pixel_size.y) * 0.5);
vec2 coords = uv * pixel_size;
vec2 center = clamp(coords, vec2(radius), pixel_size - vec2(radius));
float dist = distance(coords, center);
return 1.0 - smoothstep(radius - 0.5, radius + 0.5, dist);
}
void main() { void main() {
vec3 blurred = texture2D(tex, v_texcoord).rgb; vec3 blurred = texture2D(tex, v_texcoord).rgb;
@ -12,6 +26,7 @@ void main() {
float in_range = step(0.0, uv.x) * step(uv.x, 1.0) float in_range = step(0.0, uv.x) * step(uv.x, 1.0)
* step(0.0, uv.y) * step(uv.y, 1.0); * step(0.0, uv.y) * step(uv.y, 1.0);
float a = texture2D(mask_tex, clamp(uv, 0.0, 1.0)).a * in_range; float a = texture2D(mask_tex, clamp(uv, 0.0, 1.0)).a * in_range;
float weight = smoothstep(0.0, max(threshold, 0.001), a); float mask_weight = mix(1.0, smoothstep(0.0, max(threshold, 0.001), a), mask_enabled);
float weight = mask_weight * rounded_alpha(v_texcoord);
gl_FragColor = vec4(blurred * weight, weight); gl_FragColor = vec4(blurred * weight, weight);
} }

View file

@ -32,6 +32,8 @@ pub(super) struct BlurMaskRecord<'a> {
pub(super) mask_source_points: [[f32; 2]; 4], pub(super) mask_source_points: [[f32; 2]; 4],
pub(super) target_points: [[f32; 2]; 4], pub(super) target_points: [[f32; 2]; 4],
pub(super) threshold: f32, pub(super) threshold: f32,
pub(super) corner_radius: f32,
pub(super) pixel_size: [f32; 2],
pub(super) _phantom: std::marker::PhantomData<&'a ()>, pub(super) _phantom: std::marker::PhantomData<&'a ()>,
} }
@ -145,6 +147,9 @@ impl VulkanRenderer {
mask: Option<&BlurMaskRecord<'_>>, mask: Option<&BlurMaskRecord<'_>>,
cached_blur: Option<&Rc<VulkanImage>>, cached_blur: Option<&Rc<VulkanImage>>,
out_blur_image: &mut Option<Rc<VulkanImage>>, out_blur_image: &mut Option<Rc<VulkanImage>>,
corner_radius: f32,
pixel_size: [f32; 2],
target_points: [[f32; 2]; 4],
) -> Result<(), VulkanError> { ) -> Result<(), VulkanError> {
let [x1, y1, x2, y2] = rect; let [x1, y1, x2, y2] = rect;
let x1 = x1.max(0).min(target.width as i32); let x1 = x1.max(0).min(target.width as i32);
@ -414,7 +419,7 @@ impl VulkanRenderer {
// After cascade: levels[0] in SHADER_READ_ONLY, target in TRANSFER_SRC. // After cascade: levels[0] in SHADER_READ_ONLY, target in TRANSFER_SRC.
if let Some(mask) = mask { if mask.is_some() || corner_radius > 0.0 {
// Masked composite path: restore target to COLOR_ATTACHMENT and // Masked composite path: restore target to COLOR_ATTACHMENT and
// draw the composite shader sampling levels[0] + mask. // draw the composite shader sampling levels[0] + mask.
do_barriers(&[barrier( do_barriers(&[barrier(
@ -464,18 +469,22 @@ impl VulkanRenderer {
// Identity uv across blurred level 0 (full image is the blurred rect). // Identity uv across blurred level 0 (full image is the blurred rect).
let blurred_tc: [[f32; 2]; 4] = [[1.0, 0.0], [0.0, 0.0], [1.0, 1.0], [0.0, 1.0]]; let blurred_tc: [[f32; 2]; 4] = [[1.0, 0.0], [0.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
let mask_points = mask.map(|m| m.mask_source_points).unwrap_or(blurred_tc);
let push = BlurCompositePushConstants { let push = BlurCompositePushConstants {
pos: mask.target_points, pos: mask.map(|m| m.target_points).unwrap_or(target_points),
blurred_tex_pos: blurred_tc, blurred_tex_pos: blurred_tc,
mask_tex_pos: mask.mask_source_points, mask_tex_pos: mask_points,
threshold: mask.threshold, threshold: mask.map(|m| m.threshold).unwrap_or(1.0),
corner_radius,
pixel_size,
mask_enabled: if mask.is_some() { 1.0 } else { 0.0 },
}; };
let blurred_image_info = DescriptorImageInfo::default() let blurred_image_info = DescriptorImageInfo::default()
.image_view(levels[0].texture_view) .image_view(levels[0].texture_view)
.image_layout(ImageLayout::SHADER_READ_ONLY_OPTIMAL); .image_layout(ImageLayout::SHADER_READ_ONLY_OPTIMAL);
let mask_image_info = DescriptorImageInfo::default() let mask_image_info = DescriptorImageInfo::default()
.image_view(mask.mask_view) .image_view(mask.map(|m| m.mask_view).unwrap_or(levels[0].texture_view))
.image_layout(ImageLayout::SHADER_READ_ONLY_OPTIMAL); .image_layout(ImageLayout::SHADER_READ_ONLY_OPTIMAL);
let writes = [ let writes = [
WriteDescriptorSet::default() WriteDescriptorSet::default()
@ -671,6 +680,9 @@ impl VulkanRenderer {
blurred_tex_pos: blurred_tc, blurred_tex_pos: blurred_tc,
mask_tex_pos: mask.mask_source_points, mask_tex_pos: mask.mask_source_points,
threshold: mask.threshold, threshold: mask.threshold,
corner_radius: mask.corner_radius,
pixel_size: mask.pixel_size,
mask_enabled: 1.0,
}; };
let blurred_image_info = DescriptorImageInfo::default() let blurred_image_info = DescriptorImageInfo::default()
.image_view(cached.texture_view) .image_view(cached.texture_view)

View file

@ -27,16 +27,15 @@ use {
semaphore::VulkanSemaphore, semaphore::VulkanSemaphore,
shaders::{ shaders::{
BLUR_COMPOSITE_FRAG, BLUR_COMPOSITE_VERT, BLUR_DOWN_FRAG, BLUR_UP_FRAG, BLUR_VERT, BLUR_COMPOSITE_FRAG, BLUR_COMPOSITE_VERT, BLUR_DOWN_FRAG, BLUR_UP_FRAG, BLUR_VERT,
BlurCompositePushConstants, BlurPushConstants, BlurCompositePushConstants, BlurPushConstants, ColorManagementData, EotfArgs,
ColorManagementData, EotfArgs, FILL_FRAG, FILL_VERT, FillPushConstants, FILL_FRAG, FILL_VERT, FillPushConstants, InvEotfArgs, LEGACY_FILL_FRAG,
InvEotfArgs, LEGACY_FILL_FRAG, LEGACY_FILL_VERT, LEGACY_ROUNDED_FILL_FRAG, LEGACY_FILL_VERT, LEGACY_ROUNDED_FILL_FRAG, LEGACY_ROUNDED_FILL_VERT,
LEGACY_ROUNDED_FILL_VERT, LEGACY_ROUNDED_TEX_FRAG, LEGACY_ROUNDED_TEX_VERT, LEGACY_ROUNDED_TEX_FRAG, LEGACY_ROUNDED_TEX_VERT, LEGACY_TEX_FRAG, LEGACY_TEX_VERT,
LEGACY_TEX_FRAG, LEGACY_TEX_VERT, LegacyFillPushConstants, LegacyFillPushConstants, LegacyRoundedFillPushConstants,
LegacyRoundedFillPushConstants, LegacyRoundedTexPushConstants, LegacyRoundedTexPushConstants, LegacyTexPushConstants, OUT_FRAG, OUT_VERT,
LegacyTexPushConstants, OUT_FRAG, OUT_VERT, OutPushConstants, ROUNDED_FILL_FRAG, OutPushConstants, ROUNDED_FILL_FRAG, ROUNDED_FILL_VERT, ROUNDED_TEX_FRAG,
ROUNDED_FILL_VERT, ROUNDED_TEX_FRAG, ROUNDED_TEX_VERT, RoundedFillPushConstants, ROUNDED_TEX_VERT, RoundedFillPushConstants, RoundedTexPushConstants, TEX_FRAG,
RoundedTexPushConstants, TEX_FRAG, TEX_VERT, TexPushConstants, TexVertex, TEX_VERT, TexPushConstants, TexVertex, VulkanShader,
VulkanShader,
}, },
}, },
io_uring::IoUring, io_uring::IoUring,
@ -241,6 +240,8 @@ struct VulkanBlurOp {
cache: Option<Rc<RefCell<Option<crate::gfx_api::BlurCacheEntry>>>>, cache: Option<Rc<RefCell<Option<crate::gfx_api::BlurCacheEntry>>>>,
cache_epoch: u64, cache_epoch: u64,
cache_pixel_rect: [i32; 4], cache_pixel_rect: [i32; 4],
corner_radius: f32,
pixel_size: [f32; 2],
} }
struct VulkanBlurMask { struct VulkanBlurMask {
@ -1569,6 +1570,8 @@ impl VulkanRenderer {
cache: b.cache.clone(), cache: b.cache.clone(),
cache_epoch: b.cache_epoch, cache_epoch: b.cache_epoch,
cache_pixel_rect: b.cache_pixel_rect, cache_pixel_rect: b.cache_pixel_rect,
corner_radius: b.corner_radius,
pixel_size: b.pixel_size,
})); }));
} }
} }
@ -2305,6 +2308,8 @@ impl VulkanRenderer {
mask_source_points: m.source.to_points(), mask_source_points: m.source.to_points(),
target_points: blur.rect.to_points(), target_points: blur.rect.to_points(),
threshold: m.threshold, threshold: m.threshold,
corner_radius: blur.corner_radius,
pixel_size: blur.pixel_size,
_phantom: std::marker::PhantomData, _phantom: std::marker::PhantomData,
}); });
@ -2341,6 +2346,9 @@ impl VulkanRenderer {
mask_record.as_ref(), mask_record.as_ref(),
cached_blur.as_ref(), cached_blur.as_ref(),
&mut produced_blur, &mut produced_blur,
blur.corner_radius,
blur.pixel_size,
blur.rect.to_points(),
)?; )?;
// On a masked cache miss, store the freshly-blurred image // On a masked cache miss, store the freshly-blurred image
@ -2922,7 +2930,23 @@ impl VulkanRenderer {
// but they do paint pixels and need paint regions. // but they do paint pixels and need paint regions.
(false, rf.rect) (false, rf.rect)
} }
GfxApiOpt::RoundedCopyTexture(ct) => (false, ct.target), GfxApiOpt::RoundedCopyTexture(c) => {
let opaque = 'opaque: {
if let Some(a) = c.alpha
&& a < 1.0
{
break 'opaque false;
}
if !c.opaque {
let tex = c.tex.as_vk(&self.device.device)?;
if tex.format.has_alpha {
break 'opaque false;
}
}
true
};
(opaque, c.target)
}
GfxApiOpt::BlurBackdrop(b) => { GfxApiOpt::BlurBackdrop(b) => {
blur_rects.push(b.rect.to_rect(width, height)); blur_rects.push(b.rect.to_rect(width, height));
continue; continue;

View file

@ -198,6 +198,9 @@ pub struct BlurCompositePushConstants {
pub blurred_tex_pos: [[f32; 2]; 4], pub blurred_tex_pos: [[f32; 2]; 4],
pub mask_tex_pos: [[f32; 2]; 4], pub mask_tex_pos: [[f32; 2]; 4],
pub threshold: f32, pub threshold: f32,
pub corner_radius: f32,
pub pixel_size: [f32; 2],
pub mask_enabled: f32,
} }
unsafe impl Packed for BlurCompositePushConstants {} unsafe impl Packed for BlurCompositePushConstants {}

View file

@ -5,18 +5,33 @@ layout(set = 0, binding = 1) uniform sampler2D mask_tex;
layout(push_constant, std430) uniform Data { layout(push_constant, std430) uniform Data {
layout(offset = 96) float threshold; layout(offset = 96) float threshold;
layout(offset = 100) float corner_radius;
layout(offset = 104) vec2 pixel_size;
layout(offset = 112) float mask_enabled;
} data; } data;
layout(location = 0) in vec2 v_blurred_tex_pos; layout(location = 0) in vec2 v_blurred_tex_pos;
layout(location = 1) in vec2 v_mask_tex_pos; layout(location = 1) in vec2 v_mask_tex_pos;
layout(location = 0) out vec4 out_color; layout(location = 0) out vec4 out_color;
float rounded_alpha(vec2 uv) {
if (data.corner_radius <= 0.0)
return 1.0;
float radius = min(data.corner_radius, min(data.pixel_size.x, data.pixel_size.y) * 0.5);
vec2 coords = uv * data.pixel_size;
vec2 center = clamp(coords, vec2(radius), data.pixel_size - vec2(radius));
float dist = distance(coords, center);
return 1.0 - smoothstep(radius - 0.5, radius + 0.5, dist);
}
void main() { void main() {
vec3 blurred = textureLod(blurred_tex, v_blurred_tex_pos, 0).rgb; vec3 blurred = textureLod(blurred_tex, v_blurred_tex_pos, 0).rgb;
vec2 uv = v_mask_tex_pos; vec2 uv = v_mask_tex_pos;
float in_range = step(0.0, uv.x) * step(uv.x, 1.0) float in_range = step(0.0, uv.x) * step(uv.x, 1.0)
* step(0.0, uv.y) * step(uv.y, 1.0); * step(0.0, uv.y) * step(uv.y, 1.0);
float a = textureLod(mask_tex, clamp(uv, 0.0, 1.0), 0).a * in_range; float a = textureLod(mask_tex, clamp(uv, 0.0, 1.0), 0).a * in_range;
float weight = smoothstep(0.0, max(data.threshold, 0.001), a); float mask_weight = mix(1.0, smoothstep(0.0, max(data.threshold, 0.001), a), data.mask_enabled);
float weight = mask_weight * rounded_alpha(v_blurred_tex_pos);
out_color = vec4(blurred * weight, weight); out_color = vec4(blurred * weight, weight);
} }

View file

@ -1,6 +1,6 @@
302a9f250bdc4f8e0e71a9f77c9a8a7aa55fd003bc91c2422a700c4abd83f54e src/gfx_apis/vulkan/shaders/alpha_modes.glsl 302a9f250bdc4f8e0e71a9f77c9a8a7aa55fd003bc91c2422a700c4abd83f54e src/gfx_apis/vulkan/shaders/alpha_modes.glsl
65acbe7a6496279fa22f520ad2036d3e14a7cb1707c6a509ce7858adc4a2dcba src/gfx_apis/vulkan/shaders/blur.vert 65acbe7a6496279fa22f520ad2036d3e14a7cb1707c6a509ce7858adc4a2dcba src/gfx_apis/vulkan/shaders/blur.vert
16ad6f1eb029ccce5e0204a7d79709b05a8a708133feaf8bb20a24371de25ed7 src/gfx_apis/vulkan/shaders/blur_composite.frag e2ff39a764534debaa5857f7434fb552e9c10a83bab5d706c14240d4e704f7d2 src/gfx_apis/vulkan/shaders/blur_composite.frag
6399e23afa2e07c98b9fd1a4e853ea974a9958547ce65734846483bd7cbc8461 src/gfx_apis/vulkan/shaders/blur_composite.vert 6399e23afa2e07c98b9fd1a4e853ea974a9958547ce65734846483bd7cbc8461 src/gfx_apis/vulkan/shaders/blur_composite.vert
a04b2453c39efb018754fc25d45a369b5813359c55fad1c99020804cbb3a18e0 src/gfx_apis/vulkan/shaders/blur_down.frag a04b2453c39efb018754fc25d45a369b5813359c55fad1c99020804cbb3a18e0 src/gfx_apis/vulkan/shaders/blur_down.frag
f6d51f3b5410387d1474529c44e71bfdc31ceb80174ea6e3e4c2df30d03f11c3 src/gfx_apis/vulkan/shaders/blur_up.frag f6d51f3b5410387d1474529c44e71bfdc31ceb80174ea6e3e4c2df30d03f11c3 src/gfx_apis/vulkan/shaders/blur_up.frag

View file

@ -1494,7 +1494,7 @@ impl WlSurface {
if let Some(tl) = self.toplevel.get() { if let Some(tl) = self.toplevel.get() {
damage = damage.intersect(tl.node_absolute_position()); damage = damage.intersect(tl.node_absolute_position());
} }
self.client.state.damage(damage); self.client.state.damage_for_surface(self, damage);
} }
} }
if self.visible.get() { if self.visible.get() {
@ -1520,7 +1520,7 @@ impl WlSurface {
// Frame requests must be dispatched at the highest possible frame rate. // Frame requests must be dispatched at the highest possible frame rate.
// Therefore we must trigger a vsync of the output as soon as possible. // Therefore we must trigger a vsync of the output as soon as possible.
let rect = output.global.pos.get(); let rect = output.global.pos.get();
self.client.state.damage(rect); self.client.state.damage_for_surface(self, rect);
} }
} else { } else {
if fifo_barrier_set { if fifo_barrier_set {
@ -1555,7 +1555,7 @@ impl WlSurface {
had_texture had_texture
} }
fn apply_damage(&self, pending: &PendingState) { fn apply_damage(self: &Rc<Self>, pending: &PendingState) {
let bounds = self.toplevel.get().and_then(|tl| tl.tl_render_bounds()); let bounds = self.toplevel.get().and_then(|tl| tl.tl_render_bounds());
let pos = self.buffer_abs_pos.get(); let pos = self.buffer_abs_pos.get();
let apply_damage = |pos: Rect| { let apply_damage = |pos: Rect| {
@ -1564,7 +1564,7 @@ impl WlSurface {
if let Some(bounds) = bounds { if let Some(bounds) = bounds {
damage = damage.intersect(bounds); damage = damage.intersect(bounds);
} }
self.client.state.damage(damage); self.client.state.damage_for_surface(self, damage);
} else { } else {
let matrix = self.damage_matrix.get(); let matrix = self.damage_matrix.get();
if let Some(buffer) = self.buffer.get() { if let Some(buffer) = self.buffer.get() {
@ -1577,7 +1577,7 @@ impl WlSurface {
if let Some(bounds) = bounds { if let Some(bounds) = bounds {
damage = damage.intersect(bounds); damage = damage.intersect(bounds);
} }
self.client.state.damage(damage); self.client.state.damage_for_surface(self, damage);
} }
} }
for damage in &pending.surface_damage { for damage in &pending.surface_damage {
@ -1590,7 +1590,7 @@ impl WlSurface {
damage = Rect::new_saturating(x1, y1, x2, y2); damage = Rect::new_saturating(x1, y1, x2, y2);
} }
damage = damage.intersect(bounds.unwrap_or(pos)); damage = damage.intersect(bounds.unwrap_or(pos));
self.client.state.damage(damage); self.client.state.damage_for_surface(self, damage);
} }
} }
}; };

View file

@ -246,7 +246,6 @@ where
dx * dx + dy * dy dx * dx + dy * dy
} }
#[expect(dead_code)]
pub fn contains_rect<U>(&self, rect: &Rect<U>) -> bool pub fn contains_rect<U>(&self, rect: &Rect<U>) -> bool
where where
U: Tag, U: Tag,

View file

@ -346,7 +346,7 @@ impl Renderer<'_> {
self.render_tl_aux(placeholder.tl_data(), bounds, true); self.render_tl_aux(placeholder.tl_data(), bounds, true);
} }
fn render_tab_bar(&mut self, tab_bar: &TabBar, x: i32, y: i32, _container_width: i32) { fn render_tab_bar(&mut self, tab_bar: &TabBar, x: i32, y: i32, container_width: i32) {
let srgb_srgb = self.state.color_manager.srgb_gamma22(); let srgb_srgb = self.state.color_manager.srgb_gamma22();
let srgb = &srgb_srgb.linear; let srgb = &srgb_srgb.linear;
let perceptual = RenderIntent::Perceptual; let perceptual = RenderIntent::Perceptual;
@ -355,100 +355,160 @@ impl Renderer<'_> {
let radius = self.state.theme.sizes.tab_bar_radius.get(); let radius = self.state.theme.sizes.tab_bar_radius.get();
let border_width = self.state.theme.sizes.tab_bar_border_width.get(); let border_width = self.state.theme.sizes.tab_bar_border_width.get();
let text_padding = self.state.theme.sizes.tab_bar_text_padding.get(); let text_padding = self.state.theme.sizes.tab_bar_text_padding.get();
let padding = self.state.theme.sizes.tab_bar_padding.get();
let bar_height = tab_bar.height; let bar_height = tab_bar.height;
let render_scale = tab_bar.render_scale; let render_scale = tab_bar.render_scale;
let tab_opacity = self.state.theme.sizes.tab_opacity.get() as f32 / 100.0;
let render_text = self.state.theme.tab_render_text.get();
// Vulkan sorts ops: Fill < RoundedFill (by z_order, color) < Tex/RoundedTex (by index). // Compute static positions for non-destroying entries.
// We use: let active_entries: Vec<usize> = tab_bar
// FillRect tiny strip for Vulkan paint regions (hidden) .entries
// RoundedFillRect z0 solid rounded bg .iter()
// RoundedFillRect z1 rounded border ring (on top of bg) .enumerate()
// RoundedCopyTexture title text (on top of everything) .filter(|(_, e)| !e.destroying)
for entry in &tab_bar.entries { .map(|(i, _)| i)
let (bg_color, border_color, _text_color) = TabBar::entry_colors(self.state, entry); .collect();
let ex = entry.x.get(); let n = active_entries.len() as i32;
let ew = entry.width.get(); if n == 0 {
let tab_rect = Rect::new_sized_saturating(ex, 0, ew, bar_height); return;
let tab_cr = CornerRadius::from(radius as f32); }
let total_padding = padding * (n + 1);
let available = (container_width - total_padding).max(0);
let per_tab = available / n;
let mut remainder = available - per_tab * n;
// Tiny FillRect strip to establish Vulkan paint regions (visually hidden // Build a map: entry index → (x, width) in pixels.
// behind the RoundedFillRect bg that renders later). let mut positions: Vec<(i32, i32)> = vec![(0, 0); tab_bar.entries.len()];
let strip = Rect::new_sized_saturating( let mut tab_x = padding;
ex + radius, for &idx in &active_entries {
bar_height / 2, let w = if remainder > 0 {
(ew - 2 * radius).max(1), remainder -= 1;
1, per_tab + 1
); } else {
self.base per_tab
.fill_boxes2(slice::from_ref(&strip), &bg_color, srgb, perceptual, x, y); };
positions[idx] = (tab_x, w);
tab_x += w + padding;
}
// Rounded solid bg fill (z_order=0, renders first among RoundedFill). let render_entry =
self.base.fill_rounded_rect_z( |renderer: &mut Self, idx: usize, entry: &crate::tree::tab_bar::TabBarEntry| {
tab_rect.move_(x, y), let fade = entry.fade_opacity.value() * tab_opacity;
&bg_color, if fade < 1e-4 {
None, return;
srgb, }
perceptual,
tab_cr.scaled_by(scalef),
0.0,
0,
);
// Rounded border ring on top (z_order=1, renders after bg). let (ex, ew) = positions[idx];
if border_width > 0 { if ew < 1 {
self.base.fill_rounded_rect_z( return;
}
let (bg_color, border_color, _text_color) = entry.blended_colors(renderer.state);
let bg_color = bg_color * fade;
let border_color = border_color * fade;
let tab_rect = Rect::new_sized_saturating(ex, 0, ew, bar_height);
// Per-tab blur backdrop.
let cfg = renderer.state.blur_config.get();
if cfg.passes > 0 {
let blur_rect = tab_rect.move_(x, y);
let scaled = renderer.base.scale_rect(blur_rect);
renderer.base.push_blur_backdrop2(
scaled,
cfg.passes,
cfg.size,
None,
None,
0,
radius as f32 * renderer.base.scalef as f32,
);
}
let tab_cr = CornerRadius::from(radius as f32);
renderer.base.fill_rounded_rect_z(
tab_rect.move_(x, y), tab_rect.move_(x, y),
&border_color, &bg_color,
None, None,
srgb, srgb,
perceptual, perceptual,
tab_cr.scaled_by(scalef), tab_cr.scaled_by(scalef),
border_width as f32 * scalef, 0.0,
1, 0,
); );
}
// Title text as RoundedCopyTexture (sorts after all RoundedFill). if border_width > 0 {
let tex_ref = entry.title_texture.borrow(); renderer.base.fill_rounded_rect_z(
if let Some(tex) = tex_ref.as_ref() tab_rect.move_(x, y),
&& let Some(texture) = tex.texture() &border_color,
{ None,
use crate::theme::TabTitleAlign; srgb,
let (tw, _th) = texture.size(); perceptual,
let tex_width = (tw as f64 / render_scale.to_f64()).round() as i32; tab_cr.scaled_by(scalef),
let tab_inner = ew - 2 * (text_padding + border_width); border_width as f32 * scalef,
let text_x = match self.state.theme.tab_title_align.get() { 1,
TabTitleAlign::Start => x + ex + text_padding + border_width, );
TabTitleAlign::Center => { }
x + ex
+ border_width if !render_text {
+ (tab_inner.max(0) - tex_width).max(0) / 2 return;
+ text_padding.min(tab_inner.max(0) / 2) }
}
TabTitleAlign::End => { let tex_ref = entry.title_texture.borrow();
let end_x = x + ex + ew - tex_width - text_padding - border_width; if let Some(tex) = tex_ref.as_ref()
end_x.max(x + ex + border_width) && let Some(texture) = tex.texture()
} {
}; use crate::theme::TabTitleAlign;
let (tx, ty) = self.base.scale_point(text_x, y); let (tw, _th) = texture.size();
self.base.render_rounded_texture( let tex_width = (tw as f64 / render_scale.to_f64()).round() as i32;
&texture, let tab_inner = ew - 2 * (text_padding + border_width);
None, let text_x = match renderer.state.theme.tab_title_align.get() {
tx, TabTitleAlign::Start => x + ex + text_padding + border_width,
ty, TabTitleAlign::Center => {
None, x + ex
None, + border_width
render_scale, + (tab_inner.max(0) - tex_width).max(0) / 2
None, + text_padding.min(tab_inner.max(0) / 2)
None, }
AcquireSync::None, TabTitleAlign::End => {
ReleaseSync::None, let end_x = x + ex + ew - tex_width - text_padding - border_width;
self.state.color_manager.srgb_gamma22(), end_x.max(x + ex + border_width)
perceptual, }
AlphaMode::PremultipliedElectrical, };
CornerRadius::from(0.0_f32), let (tx, ty) = renderer.base.scale_point(text_x, y);
); renderer.base.render_rounded_texture(
&texture,
None,
tx,
ty,
None,
None,
render_scale,
None,
None,
AcquireSync::None,
ReleaseSync::None,
renderer.state.color_manager.srgb_gamma22(),
perceptual,
AlphaMode::PremultipliedElectrical,
CornerRadius::from(0.0_f32),
);
}
};
// Render unfocused entries first, then focused on top.
for (idx, entry) in tab_bar.entries.iter().enumerate() {
if entry.destroying || entry.focused_anim.target() >= 0.5 {
continue;
} }
render_entry(self, idx, entry);
}
for (idx, entry) in tab_bar.entries.iter().enumerate() {
if entry.destroying || entry.focused_anim.target() < 0.5 {
continue;
}
render_entry(self, idx, entry);
} }
} }
@ -466,7 +526,6 @@ impl Renderer<'_> {
self.render_container_decorations(container, x, y); self.render_container_decorations(container, x, y);
if let Some(child) = container.mono_child.get() { if let Some(child) = container.mono_child.get() {
// Render tab bar if present.
{ {
let tab_bar = container.tab_bar.borrow(); let tab_bar = container.tab_bar.borrow();
if let Some(tb) = tab_bar.as_ref() { if let Some(tb) = tab_bar.as_ref() {

View file

@ -411,6 +411,19 @@ impl RendererBase<'_> {
mask: Option<BlurMask>, mask: Option<BlurMask>,
cache: Option<Rc<std::cell::RefCell<Option<crate::gfx_api::BlurCacheEntry>>>>, cache: Option<Rc<std::cell::RefCell<Option<crate::gfx_api::BlurCacheEntry>>>>,
cache_epoch: u64, cache_epoch: u64,
) {
self.push_blur_backdrop2(rect, passes, offset, mask, cache, cache_epoch, 0.0);
}
pub fn push_blur_backdrop2(
&mut self,
rect: Rect,
passes: u8,
offset: f32,
mask: Option<BlurMask>,
cache: Option<Rc<std::cell::RefCell<Option<crate::gfx_api::BlurCacheEntry>>>>,
cache_epoch: u64,
corner_radius: f32,
) { ) {
let target = FramebufferRect::new( let target = FramebufferRect::new(
rect.x1() as f32, rect.x1() as f32,
@ -422,6 +435,10 @@ impl RendererBase<'_> {
self.fb_height, self.fb_height,
); );
let cache_pixel_rect = [rect.x1(), rect.y1(), rect.x2(), rect.y2()]; let cache_pixel_rect = [rect.x1(), rect.y1(), rect.x2(), rect.y2()];
let pixel_size = [
(rect.x2() - rect.x1()) as f32,
(rect.y2() - rect.y1()) as f32,
];
self.ops.push(GfxApiOpt::BlurBackdrop(BlurBackdrop { self.ops.push(GfxApiOpt::BlurBackdrop(BlurBackdrop {
rect: target, rect: target,
passes, passes,
@ -430,6 +447,8 @@ impl RendererBase<'_> {
cache, cache,
cache_epoch, cache_epoch,
cache_pixel_rect, cache_pixel_rect,
corner_radius,
pixel_size,
})); }));
} }
} }

View file

@ -306,10 +306,10 @@ pub struct State {
pub hyprland_global_shortcuts: CopyHashMap<(String, String), Rc<HyprlandGlobalShortcutV1>>, pub hyprland_global_shortcuts: CopyHashMap<(String, String), Rc<HyprlandGlobalShortcutV1>>,
pub layer_rules: RefCell<Vec<jay_config::_private::LayerRuleIpc>>, pub layer_rules: RefCell<Vec<jay_config::_private::LayerRuleIpc>>,
pub blur_config: Cell<jay_config::_private::BlurConfigIpc>, pub blur_config: Cell<jay_config::_private::BlurConfigIpc>,
pub blur_cache_epoch: NumCell<u64>,
pub animations_config: Cell<jay_config::_private::AnimationsConfigIpc>, pub animations_config: Cell<jay_config::_private::AnimationsConfigIpc>,
pub active_animations: RefCell<Vec<std::rc::Weak<dyn ToplevelNode>>>, pub active_animations: RefCell<Vec<std::rc::Weak<dyn ToplevelNode>>>,
pub close_snapshots: RefCell<Vec<Rc<crate::animation::Snapshot>>>, pub close_snapshots: RefCell<Vec<Rc<crate::animation::Snapshot>>>,
pub tab_animation_containers: RefCell<Vec<std::rc::Weak<crate::tree::ContainerNode>>>,
} }
// impl Drop for State { // impl Drop for State {
@ -1046,10 +1046,25 @@ impl State {
} }
pub fn damage(&self, rect: Rect) { pub fn damage(&self, rect: Rect) {
self.damage2(false, false, rect); self.damage2(false, false, None, rect);
} }
pub fn damage2(&self, cursor: bool, skip_hc: bool, rect: Rect) { /// Damage caused by `source` (or any of its subsurface descendants). The
/// source's blur-cache will not be invalidated for this damage, since a
/// surface's own contents don't sit behind its blur backdrop. Other
/// blurring surfaces are still invalidated normally.
pub fn damage_for_surface(&self, source: &Rc<crate::ifs::wl_surface::WlSurface>, rect: Rect) {
let root_id = source.get_root().node_id;
self.damage2(false, false, Some(root_id), rect);
}
pub fn damage2(
&self,
cursor: bool,
skip_hc: bool,
source_root: Option<crate::ifs::wl_surface::SurfaceNodeId>,
rect: Rect,
) {
if rect.is_empty() { if rect.is_empty() {
return; return;
} }
@ -1059,11 +1074,18 @@ impl State {
for surface in layer.iter() { for surface in layer.iter() {
if surface.blur.get() && surface.node_absolute_position().intersects(&rect) if surface.blur.get() && surface.node_absolute_position().intersects(&rect)
{ {
surface.blur_cache_epoch.fetch_add(1); // Skip invalidation when the damage comes from the
// bar itself (or its subsurfaces) — the bar's own
// pixels aren't part of its own blur backdrop.
if source_root != Some(surface.surface.node_id) {
surface.blur_cache_epoch.fetch_add(1);
}
} }
if surface.blur.get() && surface.blur_popups.get() { if surface.blur.get() && surface.blur_popups.get() {
surface.for_each_popup(|popup| { surface.for_each_popup(|popup| {
if popup.node_absolute_position().intersects(&rect) { if popup.node_absolute_position().intersects(&rect)
&& source_root != Some(popup.xdg.surface.node_id)
{
popup.blur_cache_epoch.fetch_add(1); popup.blur_cache_epoch.fetch_add(1);
} }
}); });
@ -1518,23 +1540,31 @@ impl State {
} }
} }
let mut snapshots = self.close_snapshots.borrow_mut(); let mut snapshots = self.close_snapshots.borrow_mut();
if snapshots.is_empty() { if !snapshots.is_empty() {
return; snapshots.retain(|snap| {
} if snap.close_progress(self).is_none() {
snapshots.retain(|snap| { if let Some(output) = snap.output.upgrade() {
if snap.close_progress(self).is_none() { self.damage(output.global.pos.get());
// Final damage so the snapshot's last-rendered position gets }
// repainted (clearing any leftover pixels). return false;
}
if let Some(output) = snap.output.upgrade() { if let Some(output) = snap.output.upgrade() {
self.damage(output.global.pos.get()); self.damage(output.global.pos.get());
} }
return false; true
});
}
{
let mut tab_containers = self.tab_animation_containers.borrow_mut();
if !tab_containers.is_empty() {
tab_containers.retain(|weak| {
let Some(container) = weak.upgrade() else {
return false;
};
container.tick_tab_animations()
});
} }
if let Some(output) = snap.output.upgrade() { }
self.damage(output.global.pos.get());
}
true
});
} }
pub fn output_extents_changed(&self) { pub fn output_extents_changed(&self) {

View file

@ -183,6 +183,14 @@ impl Color {
self.a >= 1.0 self.a >= 1.0
} }
pub fn alpha(&self) -> f32 {
self.a
}
pub fn set_alpha(&mut self, a: f32) {
self.a = a;
}
pub fn from_gray_srgb(g: u8) -> Self { pub fn from_gray_srgb(g: u8) -> Self {
Self::from_srgb(g, g, g) Self::from_srgb(g, g, g)
} }
@ -456,10 +464,16 @@ colors! {
highlight = (0x9d, 0x28, 0xc6, 0x7f), highlight = (0x9d, 0x28, 0xc6, 0x7f),
tab_active_background = (0x4c, 0x78, 0x99), tab_active_background = (0x4c, 0x78, 0x99),
tab_active_border = (0x28, 0x55, 0x77), tab_active_border = (0x28, 0x55, 0x77),
tab_focused_background = (0x3a, 0x5a, 0x70),
tab_focused_border = (0x28, 0x55, 0x77),
tab_inactive_background = (0x22, 0x22, 0x22), tab_inactive_background = (0x22, 0x22, 0x22),
tab_inactive_border = (0x33, 0x33, 0x33), tab_inactive_border = (0x33, 0x33, 0x33),
tab_urgent_background = (0x23, 0x09, 0x2c),
tab_urgent_border = (0x44, 0x11, 0x55),
tab_active_text = (0xff, 0xff, 0xff), tab_active_text = (0xff, 0xff, 0xff),
tab_focused_text = (0xdd, 0xdd, 0xdd),
tab_inactive_text = (0x88, 0x88, 0x88), tab_inactive_text = (0x88, 0x88, 0x88),
tab_urgent_text = (0xff, 0xff, 0xff),
tab_bar_background = (0x00, 0x00, 0x00, 0x00), tab_bar_background = (0x00, 0x00, 0x00, 0x00),
tab_attention_background = (0x23, 0x09, 0x2c), tab_attention_background = (0x23, 0x09, 0x2c),
} }
@ -486,10 +500,16 @@ impl StaticText for ThemeColor {
ThemeColor::highlight => "Highlight", ThemeColor::highlight => "Highlight",
ThemeColor::tab_active_background => "Tab Background (active)", ThemeColor::tab_active_background => "Tab Background (active)",
ThemeColor::tab_active_border => "Tab Border (active)", ThemeColor::tab_active_border => "Tab Border (active)",
ThemeColor::tab_focused_background => "Tab Background (focused)",
ThemeColor::tab_focused_border => "Tab Border (focused)",
ThemeColor::tab_inactive_background => "Tab Background (inactive)", ThemeColor::tab_inactive_background => "Tab Background (inactive)",
ThemeColor::tab_inactive_border => "Tab Border (inactive)", ThemeColor::tab_inactive_border => "Tab Border (inactive)",
ThemeColor::tab_urgent_background => "Tab Background (urgent)",
ThemeColor::tab_urgent_border => "Tab Border (urgent)",
ThemeColor::tab_active_text => "Tab Text (active)", ThemeColor::tab_active_text => "Tab Text (active)",
ThemeColor::tab_focused_text => "Tab Text (focused)",
ThemeColor::tab_inactive_text => "Tab Text (inactive)", ThemeColor::tab_inactive_text => "Tab Text (inactive)",
ThemeColor::tab_urgent_text => "Tab Text (urgent)",
ThemeColor::tab_bar_background => "Tab Bar Background", ThemeColor::tab_bar_background => "Tab Bar Background",
ThemeColor::tab_attention_background => "Tab Attention Background", ThemeColor::tab_attention_background => "Tab Attention Background",
} }
@ -610,6 +630,7 @@ sizes! {
tab_bar_border_width = (0, 1000, 2), tab_bar_border_width = (0, 1000, 2),
tab_bar_text_padding = (0, 1000, 4), tab_bar_text_padding = (0, 1000, 4),
tab_bar_gap = (0, 1000, 4), tab_bar_gap = (0, 1000, 4),
tab_opacity = (0, 100, 100),
} }
impl StaticText for ThemeSized { impl StaticText for ThemeSized {
@ -627,6 +648,7 @@ impl StaticText for ThemeSized {
ThemeSized::tab_bar_border_width => "Tab Bar Border Width", ThemeSized::tab_bar_border_width => "Tab Bar Border Width",
ThemeSized::tab_bar_text_padding => "Tab Bar Text Padding", ThemeSized::tab_bar_text_padding => "Tab Bar Text Padding",
ThemeSized::tab_bar_gap => "Tab Bar Gap", ThemeSized::tab_bar_gap => "Tab Bar Gap",
ThemeSized::tab_opacity => "Tab Opacity",
} }
} }
} }
@ -784,6 +806,8 @@ pub struct Theme {
pub corner_radius: Cell<CornerRadius>, pub corner_radius: Cell<CornerRadius>,
pub autotile_enabled: Cell<bool>, pub autotile_enabled: Cell<bool>,
pub tab_title_align: Cell<TabTitleAlign>, pub tab_title_align: Cell<TabTitleAlign>,
pub tab_from_top: Cell<bool>,
pub tab_render_text: Cell<bool>,
} }
impl Default for Theme { impl Default for Theme {
@ -802,6 +826,8 @@ impl Default for Theme {
corner_radius: Cell::new(CornerRadius::default()), corner_radius: Cell::new(CornerRadius::default()),
autotile_enabled: Cell::new(false), autotile_enabled: Cell::new(false),
tab_title_align: Cell::new(TabTitleAlign::default()), tab_title_align: Cell::new(TabTitleAlign::default()),
tab_from_top: Cell::new(false),
tab_render_text: Cell::new(true),
} }
} }
} }

View file

@ -18,10 +18,7 @@ use {
ContainingNode, Direction, FindTreeResult, FindTreeUsecase, FloatNode, FoundNode, Node, ContainingNode, Direction, FindTreeResult, FindTreeUsecase, FloatNode, FoundNode, Node,
NodeId, NodeLayerLink, NodeLocation, OutputNode, TddType, TileDragDestination, NodeId, NodeLayerLink, NodeLocation, OutputNode, TddType, TileDragDestination,
ToplevelData, ToplevelNode, ToplevelNodeBase, ToplevelType, WorkspaceNode, ToplevelData, ToplevelNode, ToplevelNodeBase, ToplevelType, WorkspaceNode,
default_tile_drag_bounds, default_tile_drag_bounds, tab_bar::TabBar, toplevel_set_workspace, walker::NodeVisitor,
tab_bar::{TabBar, TabBarEntry},
toplevel_set_workspace,
walker::NodeVisitor,
}, },
utils::{ utils::{
clonecell::CloneCell, clonecell::CloneCell,
@ -265,6 +262,9 @@ impl ContainerNode {
update_tab_textures_scheduled: Cell::new(false), update_tab_textures_scheduled: Cell::new(false),
ephemeral: Cell::new(Ephemeral::Off), ephemeral: Cell::new(Ephemeral::Off),
}); });
child_node_ref
.focus_history
.set(Some(slf.focus_history.add_last(child_node_ref.clone())));
child.tl_set_parent(slf.clone()); child.tl_set_parent(slf.clone());
slf.pull_child_properties(&child_node_ref); slf.pull_child_properties(&child_node_ref);
slf slf
@ -700,6 +700,32 @@ impl ContainerNode {
} }
} }
fn register_tab_animation(self: &Rc<Self>) {
let mut list = self.state.tab_animation_containers.borrow_mut();
let already = list
.iter()
.any(|w| w.upgrade().map_or(false, |c| Rc::ptr_eq(&c, self)));
if !already {
list.push(Rc::downgrade(self));
}
}
pub fn tick_tab_animations(&self) -> bool {
let now = self.state.now_nsec();
let cfg = self.state.animations_config.get();
let duration = if cfg.enabled { cfg.open_duration_ms } else { 0 };
let mut bar = self.tab_bar.borrow_mut();
if let Some(bar) = bar.as_mut() {
let animating = bar.tick(now, duration);
if animating {
self.damage();
}
animating
} else {
false
}
}
fn compute_render_positions(&self) { fn compute_render_positions(&self) {
self.compute_render_positions_scheduled.set(false); self.compute_render_positions_scheduled.set(false);
let mut rd = self.render_data.borrow_mut(); let mut rd = self.render_data.borrow_mut();
@ -780,6 +806,7 @@ impl ContainerNode {
if self.mono_child.is_some() == child.is_some() { if self.mono_child.is_some() == child.is_some() {
return; return;
} }
let exiting_mono = self.mono_child.is_some() && child.is_none();
let child = { let child = {
let children = self.child_nodes.borrow(); let children = self.child_nodes.borrow();
match child { match child {
@ -813,6 +840,9 @@ impl ContainerNode {
} else { } else {
for child in self.children.iter() { for child in self.children.iter() {
child.node.tl_set_visible(true); child.node.tl_set_visible(true);
if exiting_mono {
child.node.tl_data().start_open_animation();
}
} }
} }
} }
@ -913,9 +943,10 @@ impl ContainerNode {
"untitled".to_string() "untitled".to_string()
} }
/// Rebuild the tab bar. If `override_id` and `override_title` are provided, /// Update the tab bar entries to match the current children. Preserves
/// use the override title for that child instead of borrowing it (avoids /// existing entries (and their animation state) for children still present,
/// RefCell double-borrow when called from node_child_title_changed). /// starts exit animations for removed children, and creates new entries
/// with enter animations for added children.
fn rebuild_tab_bar_with_override( fn rebuild_tab_bar_with_override(
self: &Rc<Self>, self: &Rc<Self>,
override_id: Option<NodeId>, override_id: Option<NodeId>,
@ -938,24 +969,35 @@ impl ContainerNode {
.persistent .persistent
.scale .scale
.get(); .get();
let mut bar = TabBar::new(height, render_scale); let now = self.state.now_nsec();
for child in self.children.iter() {
let child_id = child.node.node_id(); let children_data: Vec<_> = self
let title = self.get_child_tab_title(&child, override_id, override_title); .children
bar.entries.push(TabBarEntry { .iter()
child_id, .map(|child| {
title, let child_id = child.node.node_id();
title_texture: Rc::new(RefCell::new(None)), let title = self.get_child_tab_title(&child, override_id, override_title);
active: child_id == active_id, let active = child_id == active_id;
attention_requested: child.attention_requested.get(), let focused = active && child.active.get();
x: Cell::new(0), let urgent = child.attention_requested.get();
width: Cell::new(0), (child_id, title, active, focused, urgent)
}); })
} .collect();
let padding = self.state.theme.sizes.tab_bar_padding.get();
bar.layout_entries(self.width.get(), padding); let mut bar_ref = self.tab_bar.borrow_mut();
*self.tab_bar.borrow_mut() = Some(bar); let bar = bar_ref.get_or_insert_with(|| TabBar::new(height, render_scale));
bar.height = height;
bar.render_scale = render_scale;
bar.update_entries(&children_data, now);
bar.update_animations();
let animating = bar.entries.iter().any(|e| e.is_animating());
drop(bar_ref);
self.schedule_update_tab_textures(); self.schedule_update_tab_textures();
if animating {
self.register_tab_animation();
}
} }
/// Wrap the focused child in a new sub-container with the given split direction. /// Wrap the focused child in a new sub-container with the given split direction.
@ -1421,8 +1463,11 @@ impl ContainerNode {
let tab_bar = self.tab_bar.borrow(); let tab_bar = self.tab_bar.borrow();
if let Some(tb) = tab_bar.as_ref() { if let Some(tb) = tab_bar.as_ref() {
if seat_data.y >= 0 && seat_data.y < tb.height { if seat_data.y >= 0 && seat_data.y < tb.height {
if let Some(idx) = tb.entry_at_x(seat_data.x) { let padding = self.state.theme.sizes.tab_bar_padding.get();
let child_id = tb.entries[idx].child_id; if let Some(idx) = tb.entry_at_x(seat_data.x, self.width.get(), padding) {
let active_entries: Vec<_> =
tb.entries.iter().filter(|e| !e.destroying).collect();
let child_id = active_entries[idx].child_id;
drop(tab_bar); drop(tab_bar);
drop(seat_datas); drop(seat_datas);
let children = self.child_nodes.borrow(); let children = self.child_nodes.borrow();
@ -1698,6 +1743,7 @@ impl ContainerNode {
let entries: Vec<_> = tb let entries: Vec<_> = tb
.entries .entries
.iter() .iter()
.filter(|e| !e.destroying)
.map(|e| { .map(|e| {
( (
e.title.clone(), e.title.clone(),
@ -1722,7 +1768,6 @@ impl ContainerNode {
if let Some(s) = scale { if let Some(s) = scale {
texture_height = (bar_height as f64 * s).round() as _; texture_height = (bar_height as f64 * s).round() as _;
} }
let mut scheduled = 0;
let mut texture_refs = Vec::new(); let mut texture_refs = Vec::new();
for (title, (_, _, text_color), title_texture) in &entries { for (title, (_, _, text_color), title_texture) in &entries {
let mut tex_ref = title_texture.borrow_mut(); let mut tex_ref = title_texture.borrow_mut();
@ -1737,7 +1782,6 @@ impl ContainerNode {
scale, scale,
); );
texture_refs.push(title_texture.clone()); texture_refs.push(title_texture.clone());
scheduled += 1;
} }
(on_completed.event(), texture_refs) (on_completed.event(), texture_refs)
} }
@ -2404,11 +2448,9 @@ impl ToplevelNodeBase for ContainerNode {
size_changed |= self.height.replace(rect.height()) != rect.height(); size_changed |= self.height.replace(rect.height()) != rect.height();
if size_changed { if size_changed {
self.update_content_size(); self.update_content_size();
// Re-layout tab bar entries when container size changes in mono mode.
if self.mono_child.is_some() { if self.mono_child.is_some() {
if let Some(bar) = self.tab_bar.borrow().as_ref() { if let Some(bar) = self.tab_bar.borrow_mut().as_mut() {
let padding = self.state.theme.sizes.tab_bar_padding.get(); bar.update_animations();
bar.layout_entries(rect.width(), padding);
} }
} }
// log::info!("tl_change_extents"); // log::info!("tl_change_extents");

View file

@ -1,11 +1,85 @@
use { use {
crate::{scale::Scale, state::State, text::TextTexture, theme::Color, tree::NodeId}, crate::{
std::{ scale::Scale,
cell::{Cell, RefCell}, state::State,
rc::Rc, text::TextTexture,
theme::{Color, Oklab},
tree::NodeId,
}, },
std::{cell::RefCell, rc::Rc},
}; };
/// A single animated float value that interpolates from `start` to `target`
/// over a configurable duration using an ease-out cubic curve.
#[derive(Clone, Debug)]
pub struct AnimatedF32 {
start: f32,
target: f32,
current: f32,
start_nsec: u64,
warped: bool,
}
impl AnimatedF32 {
pub fn new(value: f32) -> Self {
Self {
start: value,
target: value,
current: value,
start_nsec: 0,
warped: true,
}
}
pub fn set(&mut self, target: f32, now_nsec: u64) {
if (self.target - target).abs() < 1e-6 {
return;
}
self.start = self.current;
self.target = target;
self.start_nsec = now_nsec;
self.warped = false;
}
pub fn warp(&mut self, value: f32) {
self.start = value;
self.target = value;
self.current = value;
self.warped = true;
}
pub fn value(&self) -> f32 {
self.current
}
pub fn target(&self) -> f32 {
self.target
}
pub fn is_animating(&self) -> bool {
!self.warped && (self.current - self.target).abs() > 1e-5
}
pub fn tick(&mut self, now_nsec: u64, duration_ms: u32) {
if self.warped || duration_ms == 0 {
self.current = self.target;
self.warped = true;
return;
}
let elapsed = now_nsec.saturating_sub(self.start_nsec);
let dur = (duration_ms as u64).saturating_mul(1_000_000);
if elapsed >= dur {
self.current = self.target;
self.warped = true;
return;
}
let t = (elapsed as f32) / (dur as f32);
let inv = 1.0 - t;
let eased = 1.0 - inv * inv * inv;
self.current = self.start + (self.target - self.start) * eased;
}
}
/// A single entry (tab) within a tab bar. /// A single entry (tab) within a tab bar.
pub struct TabBarEntry { pub struct TabBarEntry {
/// The node ID of the child this tab represents. /// The node ID of the child this tab represents.
@ -14,19 +88,185 @@ pub struct TabBarEntry {
pub title: String, pub title: String,
/// Pre-rendered text texture for the tab title. /// Pre-rendered text texture for the tab title.
pub title_texture: Rc<RefCell<Option<TextTexture>>>, pub title_texture: Rc<RefCell<Option<TextTexture>>>,
/// Whether this is the active (visible) tab. /// Whether this entry is being destroyed (exit animation in flight).
pub active: bool, pub destroying: bool,
/// Whether this tab's window has requested attention.
pub attention_requested: bool, /// Horizontal offset as a fraction of total bar width (0.01.0).
/// X offset of this tab within the tab bar (relative to tab bar start). pub offset: AnimatedF32,
pub x: Cell<i32>, /// Width as a fraction of total bar width (0.01.0).
/// Width of this tab in pixels. pub width: AnimatedF32,
pub width: Cell<i32>, /// Vertical slide position: 0.0 = in place, 1.0 = fully slid out.
pub vertical_pos: AnimatedF32,
/// Fade opacity: 0.0 = invisible, 1.0 = fully visible.
pub fade_opacity: AnimatedF32,
/// Active (selected tab) blend weight: 0.01.0.
pub active_anim: AnimatedF32,
/// Focused (keyboard focus on this tab's container) blend weight: 0.01.0.
pub focused_anim: AnimatedF32,
/// Urgent/attention blend weight: 0.01.0.
pub urgent_anim: AnimatedF32,
}
impl TabBarEntry {
pub fn new(
child_id: NodeId,
title: String,
active: bool,
focused: bool,
urgent: bool,
now_nsec: u64,
) -> Self {
let mut entry = Self {
child_id,
title,
title_texture: Rc::new(RefCell::new(None)),
destroying: false,
offset: AnimatedF32::new(-1.0),
width: AnimatedF32::new(-1.0),
vertical_pos: AnimatedF32::new(1.0),
fade_opacity: AnimatedF32::new(0.0),
active_anim: AnimatedF32::new(if active { 1.0 } else { 0.0 }),
focused_anim: AnimatedF32::new(if focused { 1.0 } else { 0.0 }),
urgent_anim: AnimatedF32::new(if urgent { 1.0 } else { 0.0 }),
};
entry.vertical_pos.set(0.0, now_nsec);
entry.fade_opacity.set(1.0, now_nsec);
entry
}
pub fn begin_destroy(&mut self, now_nsec: u64) {
self.destroying = true;
self.vertical_pos.set(1.0, now_nsec);
self.fade_opacity.set(0.0, now_nsec);
}
pub fn un_destroy(&mut self, now_nsec: u64) {
self.destroying = false;
self.vertical_pos.set(0.0, now_nsec);
self.fade_opacity.set(1.0, now_nsec);
}
pub fn should_remove(&self) -> bool {
self.destroying && (!self.vertical_pos.is_animating() || self.width.value() < 1e-4)
}
pub fn set_active(&mut self, active: bool, _now_nsec: u64) {
let target = if active { 1.0 } else { 0.0 };
self.active_anim.warp(target);
}
pub fn set_focused(&mut self, focused: bool, _now_nsec: u64) {
let target = if focused { 1.0 } else { 0.0 };
self.focused_anim.warp(target);
}
pub fn set_urgent(&mut self, urgent: bool, _now_nsec: u64) {
let u = if urgent && self.focused_anim.target() < 0.5 {
1.0
} else {
0.0
};
self.urgent_anim.warp(u);
}
pub fn set_title(&mut self, title: String) {
if self.title != title {
self.title = title;
*self.title_texture.borrow_mut() = None;
}
}
pub fn tick_all(&mut self, now_nsec: u64, duration_ms: u32) {
self.offset.tick(now_nsec, duration_ms);
self.width.tick(now_nsec, duration_ms);
self.vertical_pos.tick(now_nsec, duration_ms);
self.fade_opacity.tick(now_nsec, duration_ms);
self.active_anim.tick(now_nsec, duration_ms);
self.focused_anim.tick(now_nsec, duration_ms);
self.urgent_anim.tick(now_nsec, duration_ms);
}
pub fn is_animating(&self) -> bool {
self.offset.is_animating()
|| self.width.is_animating()
|| self.vertical_pos.is_animating()
|| self.fade_opacity.is_animating()
|| self.active_anim.is_animating()
|| self.focused_anim.is_animating()
|| self.urgent_anim.is_animating()
}
/// Compute the blended color for this entry, interpolating in OkLab space
/// between active, focused, urgent, and inactive colors based on the
/// animated blend weights.
pub fn blended_colors(&self, state: &State) -> (Color, Color, Color) {
let theme = &state.theme;
let active_v = self.active_anim.value();
let urgent_v = (self.urgent_anim.value() - active_v).max(0.0);
let focused_v = (self.focused_anim.value() - active_v - urgent_v).max(0.0);
let inactive_v = (1.0 - active_v - urgent_v - focused_v).max(0.0);
let bg = blend_colors_oklab(&[
(active_v, theme.colors.tab_active_background.get()),
(focused_v, theme.colors.tab_focused_background.get()),
(urgent_v, theme.colors.tab_urgent_background.get()),
(inactive_v, theme.colors.tab_inactive_background.get()),
]);
let border = blend_colors_oklab(&[
(active_v, theme.colors.tab_active_border.get()),
(focused_v, theme.colors.tab_focused_border.get()),
(urgent_v, theme.colors.tab_urgent_border.get()),
(inactive_v, theme.colors.tab_inactive_border.get()),
]);
let text = blend_colors_oklab(&[
(active_v, theme.colors.tab_active_text.get()),
(focused_v, theme.colors.tab_focused_text.get()),
(urgent_v, theme.colors.tab_urgent_text.get()),
(inactive_v, theme.colors.tab_inactive_text.get()),
]);
(bg, border, text)
}
}
fn blend_colors_oklab(weighted: &[(f32, Color)]) -> Color {
let mut total_weight = 0.0f32;
let mut lab = Oklab {
l: 0.0,
a: 0.0,
b: 0.0,
};
let mut alpha = 0.0f32;
for &(w, color) in weighted {
if w < 1e-6 {
continue;
}
total_weight += w;
let c_lab = color.srgb_to_oklab();
lab.l += w * c_lab.l;
lab.a += w * c_lab.a;
lab.b += w * c_lab.b;
alpha += w * color.alpha();
}
if total_weight < 1e-6 {
return Color::TRANSPARENT;
}
let inv = 1.0 / total_weight;
lab.l *= inv;
lab.a *= inv;
lab.b *= inv;
alpha *= inv;
let mut c = lab.to_srgb();
c.set_alpha(alpha);
c
} }
/// A tab bar rendered above a container in mono (tabbed) mode. /// A tab bar rendered above a container in mono (tabbed) mode.
pub struct TabBar { pub struct TabBar {
/// The individual tab entries. /// The individual tab entries (persistent across updates).
pub entries: Vec<TabBarEntry>, pub entries: Vec<TabBarEntry>,
/// Height of the tab bar in pixels (from theme). /// Height of the tab bar in pixels (from theme).
pub height: i32, pub height: i32,
@ -35,7 +275,6 @@ pub struct TabBar {
} }
impl TabBar { impl TabBar {
/// Create a new empty tab bar.
pub fn new(height: i32, render_scale: Scale) -> Self { pub fn new(height: i32, render_scale: Scale) -> Self {
Self { Self {
entries: Vec::new(), entries: Vec::new(),
@ -44,67 +283,116 @@ impl TabBar {
} }
} }
/// Recompute the positions and widths of all tab entries. /// Update entries to match the current set of children. Preserves existing
/// /// entries (and their animation state) for children that are still present,
/// `total_width` is the available width for the entire tab bar. /// starts exit animations for removed children, and creates new entries with
pub fn layout_entries(&self, total_width: i32, padding: i32) { /// enter animations for added children.
let n = self.entries.len() as i32; pub fn update_entries(
if n == 0 { &mut self,
children: &[(NodeId, String, bool, bool, bool)],
now_nsec: u64,
) {
let mut old_entries: Vec<TabBarEntry> = self.entries.drain(..).collect();
for &(child_id, ref title, active, focused, urgent) in children {
if let Some(pos) = old_entries.iter().position(|e| e.child_id == child_id) {
let mut entry = old_entries.remove(pos);
if entry.destroying {
entry.un_destroy(now_nsec);
}
entry.set_active(active, now_nsec);
entry.set_focused(focused, now_nsec);
entry.set_urgent(urgent, now_nsec);
entry.set_title(title.clone());
self.entries.push(entry);
} else {
let mut entry =
TabBarEntry::new(child_id, title.clone(), active, focused, urgent, now_nsec);
entry.vertical_pos.warp(0.0);
entry.fade_opacity.warp(1.0);
self.entries.push(entry);
}
}
for mut entry in old_entries {
if !entry.destroying {
entry.begin_destroy(now_nsec);
}
if entry.should_remove() {
continue;
}
self.entries.push(entry);
}
}
/// Recompute offset/width for all entries. All values are warped
/// to their final positions immediately (no animation on position).
pub fn update_animations(&mut self) {
let active_count = self.entries.iter().filter(|e| !e.destroying).count();
if active_count == 0 {
return; return;
} }
let entry_width = 1.0 / active_count as f32;
let mut offset = 0.0f32;
for entry in &mut self.entries {
if !entry.destroying {
entry.offset.warp(offset);
entry.width.warp(entry_width);
offset += entry_width;
}
}
}
/// Tick all animations forward and remove completed exit animations.
/// Returns true if any animation is still in flight.
pub fn tick(&mut self, now_nsec: u64, duration_ms: u32) -> bool {
for entry in &mut self.entries {
entry.tick_all(now_nsec, duration_ms);
}
self.entries.retain(|e| !e.should_remove());
self.is_animating()
}
pub fn is_animating(&self) -> bool {
self.entries.iter().any(|e| e.is_animating())
}
/// Find the tab entry index at the given x coordinate (relative to tab bar).
/// Only considers non-destroying entries.
pub fn entry_at_x(&self, x: i32, total_width: i32, padding: i32) -> Option<usize> {
let active_entries: Vec<_> = self
.entries
.iter()
.enumerate()
.filter(|(_, e)| !e.destroying)
.collect();
let n = active_entries.len() as i32;
if n == 0 {
return None;
}
let total_padding = padding * (n + 1); let total_padding = padding * (n + 1);
let available = (total_width - total_padding).max(0); let available = (total_width - total_padding).max(0);
let per_tab = available / n; let per_tab = available / n;
let mut remainder = available - per_tab * n; let mut remainder = available - per_tab * n;
let mut x = padding; let mut tab_x = padding;
for entry in &self.entries { for (i, (_idx, _entry)) in active_entries.iter().enumerate() {
let w = if remainder > 0 { let w = if remainder > 0 {
remainder -= 1; remainder -= 1;
per_tab + 1 per_tab + 1
} else { } else {
per_tab per_tab
}; };
entry.x.set(x); if x >= tab_x && x < tab_x + w {
entry.width.set(w);
x += w + padding;
}
}
/// Find the tab entry index at the given x coordinate (relative to tab bar).
///
/// Returns `None` if the coordinate is in padding between tabs or out of bounds.
pub fn entry_at_x(&self, x: i32) -> Option<usize> {
for (i, entry) in self.entries.iter().enumerate() {
let ex = entry.x.get();
let ew = entry.width.get();
if x >= ex && x < ex + ew {
return Some(i); return Some(i);
} }
tab_x += w + padding;
} }
None None
} }
/// Get the colors for a tab entry based on its state. /// Get bg, border, text colors.
pub fn entry_colors(state: &State, entry: &TabBarEntry) -> (Color, Color, Color) { pub fn entry_colors(state: &State, entry: &TabBarEntry) -> (Color, Color, Color) {
let theme = &state.theme; entry.blended_colors(state)
if entry.attention_requested {
(
theme.colors.tab_attention_background.get(),
theme.colors.tab_inactive_border.get(),
theme.colors.tab_active_text.get(),
)
} else if entry.active {
(
theme.colors.tab_active_background.get(),
theme.colors.tab_active_border.get(),
theme.colors.tab_active_text.get(),
)
} else {
(
theme.colors.tab_inactive_background.get(),
theme.colors.tab_inactive_border.get(),
theme.colors.tab_inactive_text.get(),
)
}
} }
} }

View file

@ -241,10 +241,16 @@ pub struct Theme {
pub corner_radius: Option<f32>, pub corner_radius: Option<f32>,
pub tab_active_bg_color: Option<Color>, pub tab_active_bg_color: Option<Color>,
pub tab_active_border_color: Option<Color>, pub tab_active_border_color: Option<Color>,
pub tab_focused_bg_color: Option<Color>,
pub tab_focused_border_color: Option<Color>,
pub tab_inactive_bg_color: Option<Color>, pub tab_inactive_bg_color: Option<Color>,
pub tab_inactive_border_color: Option<Color>, pub tab_inactive_border_color: Option<Color>,
pub tab_urgent_bg_color: Option<Color>,
pub tab_urgent_border_color: Option<Color>,
pub tab_active_text_color: Option<Color>, pub tab_active_text_color: Option<Color>,
pub tab_focused_text_color: Option<Color>,
pub tab_inactive_text_color: Option<Color>, pub tab_inactive_text_color: Option<Color>,
pub tab_urgent_text_color: Option<Color>,
pub tab_bar_bg_color: Option<Color>, pub tab_bar_bg_color: Option<Color>,
pub tab_attention_bg_color: Option<Color>, pub tab_attention_bg_color: Option<Color>,
pub tab_bar_height: Option<i32>, pub tab_bar_height: Option<i32>,
@ -253,7 +259,10 @@ pub struct Theme {
pub tab_bar_border_width: Option<i32>, pub tab_bar_border_width: Option<i32>,
pub tab_bar_text_padding: Option<i32>, pub tab_bar_text_padding: Option<i32>,
pub tab_bar_gap: Option<i32>, pub tab_bar_gap: Option<i32>,
pub tab_opacity: Option<i32>,
pub tab_title_align: Option<String>, pub tab_title_align: Option<String>,
pub tab_from_top: Option<bool>,
pub tab_render_text: Option<bool>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -398,6 +407,7 @@ pub enum AnimationCurve {
Linear, Linear,
EaseOut, EaseOut,
EaseInOut, EaseInOut,
#[allow(dead_code)]
Bezier { x1: f32, y1: f32, x2: f32, y2: f32 }, Bezier { x1: f32, y1: f32, x2: f32, y2: f32 },
} }

View file

@ -105,41 +105,58 @@ impl Parser for ThemeParser<'_> {
( (
tab_active_bg_color, tab_active_bg_color,
tab_active_border_color, tab_active_border_color,
tab_focused_bg_color,
tab_focused_border_color,
tab_inactive_bg_color, tab_inactive_bg_color,
tab_inactive_border_color, tab_inactive_border_color,
tab_urgent_bg_color,
tab_urgent_border_color,
tab_active_text_color, tab_active_text_color,
tab_focused_text_color,
),
(
tab_inactive_text_color, tab_inactive_text_color,
tab_urgent_text_color,
tab_bar_bg_color, tab_bar_bg_color,
tab_attention_bg_color, tab_attention_bg_color,
tab_bar_height, tab_bar_height,
tab_bar_padding, tab_bar_padding,
),
(
tab_bar_radius, tab_bar_radius,
tab_bar_border_width, tab_bar_border_width,
tab_bar_text_padding, tab_bar_text_padding,
tab_bar_gap, tab_bar_gap,
tab_title_align_val,
), ),
(tab_opacity, tab_title_align_val, tab_from_top, tab_render_text),
) = ext.extract(( ) = ext.extract((
( (
opt(val("tab-active-bg-color")), opt(val("tab-active-bg-color")),
opt(val("tab-active-border-color")), opt(val("tab-active-border-color")),
opt(val("tab-focused-bg-color")),
opt(val("tab-focused-border-color")),
opt(val("tab-inactive-bg-color")), opt(val("tab-inactive-bg-color")),
opt(val("tab-inactive-border-color")), opt(val("tab-inactive-border-color")),
opt(val("tab-urgent-bg-color")),
opt(val("tab-urgent-border-color")),
opt(val("tab-active-text-color")), opt(val("tab-active-text-color")),
opt(val("tab-focused-text-color")),
),
(
opt(val("tab-inactive-text-color")), opt(val("tab-inactive-text-color")),
opt(val("tab-urgent-text-color")),
opt(val("tab-bar-bg-color")), opt(val("tab-bar-bg-color")),
opt(val("tab-attention-bg-color")), opt(val("tab-attention-bg-color")),
recover(opt(s32("tab-bar-height"))), recover(opt(s32("tab-bar-height"))),
recover(opt(s32("tab-bar-padding"))), recover(opt(s32("tab-bar-padding"))),
),
(
recover(opt(s32("tab-bar-radius"))), recover(opt(s32("tab-bar-radius"))),
recover(opt(s32("tab-bar-border-width"))), recover(opt(s32("tab-bar-border-width"))),
recover(opt(s32("tab-bar-text-padding"))), recover(opt(s32("tab-bar-text-padding"))),
recover(opt(s32("tab-bar-gap"))), recover(opt(s32("tab-bar-gap"))),
),
(
recover(opt(s32("tab-opacity"))),
recover(opt(str("tab-title-align"))), recover(opt(str("tab-title-align"))),
recover(opt(bol("tab-from-top"))),
recover(opt(bol("tab-render-text"))),
), ),
))?; ))?;
macro_rules! color { macro_rules! color {
@ -199,10 +216,16 @@ impl Parser for ThemeParser<'_> {
corner_radius: corner_radius.map(|v| v.value as f32), corner_radius: corner_radius.map(|v| v.value as f32),
tab_active_bg_color: color!(tab_active_bg_color), tab_active_bg_color: color!(tab_active_bg_color),
tab_active_border_color: color!(tab_active_border_color), tab_active_border_color: color!(tab_active_border_color),
tab_focused_bg_color: color!(tab_focused_bg_color),
tab_focused_border_color: color!(tab_focused_border_color),
tab_inactive_bg_color: color!(tab_inactive_bg_color), tab_inactive_bg_color: color!(tab_inactive_bg_color),
tab_inactive_border_color: color!(tab_inactive_border_color), tab_inactive_border_color: color!(tab_inactive_border_color),
tab_urgent_bg_color: color!(tab_urgent_bg_color),
tab_urgent_border_color: color!(tab_urgent_border_color),
tab_active_text_color: color!(tab_active_text_color), tab_active_text_color: color!(tab_active_text_color),
tab_focused_text_color: color!(tab_focused_text_color),
tab_inactive_text_color: color!(tab_inactive_text_color), tab_inactive_text_color: color!(tab_inactive_text_color),
tab_urgent_text_color: color!(tab_urgent_text_color),
tab_bar_bg_color: color!(tab_bar_bg_color), tab_bar_bg_color: color!(tab_bar_bg_color),
tab_attention_bg_color: color!(tab_attention_bg_color), tab_attention_bg_color: color!(tab_attention_bg_color),
tab_bar_height: tab_bar_height.despan(), tab_bar_height: tab_bar_height.despan(),
@ -211,7 +234,10 @@ impl Parser for ThemeParser<'_> {
tab_bar_border_width: tab_bar_border_width.despan(), tab_bar_border_width: tab_bar_border_width.despan(),
tab_bar_text_padding: tab_bar_text_padding.despan(), tab_bar_text_padding: tab_bar_text_padding.despan(),
tab_bar_gap: tab_bar_gap.despan(), tab_bar_gap: tab_bar_gap.despan(),
tab_opacity: tab_opacity.despan(),
tab_title_align: tab_title_align_val.map(|v| v.value.to_string()), tab_title_align: tab_title_align_val.map(|v| v.value.to_string()),
tab_from_top: tab_from_top.despan(),
tab_render_text: tab_render_text.despan(),
}) })
} }
} }

View file

@ -14,9 +14,9 @@ use {
crate::{ crate::{
config::{ config::{
Action, AnimationCurve, AnimationsConfig, BlurConfig, ClientRule, Config, Action, AnimationCurve, AnimationsConfig, BlurConfig, ClientRule, Config,
ConfigConnector, ConfigDrmDevice, ConfigKeymap, ConfigConnector, ConfigDrmDevice, ConfigKeymap, ConnectorMatch, DrmDeviceMatch, Exec,
ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, LayerKind, LayerRule, Output, Input, InputMatch, LayerKind, LayerRule, Output, OutputMatch, SimpleCommand, Status,
OutputMatch, SimpleCommand, Status, Theme, WindowRule, parse_config, Theme, WindowRule, parse_config,
}, },
rules::{MatcherTemp, RuleMapper}, rules::{MatcherTemp, RuleMapper},
shortcuts::ModeState, shortcuts::ModeState,
@ -47,8 +47,8 @@ use {
set_color_management_enabled, set_corner_radius, set_default_workspace_capture, set_color_management_enabled, set_corner_radius, set_default_workspace_capture,
set_explicit_sync_enabled, set_float_above_fullscreen, set_floating_titles, set_idle, set_explicit_sync_enabled, set_float_above_fullscreen, set_floating_titles, set_idle,
set_idle_grace_period, set_middle_click_paste_enabled, set_show_bar, set_idle_grace_period, set_middle_click_paste_enabled, set_show_bar,
set_show_float_pin_icon, set_show_titles, set_tab_title_align, set_ui_drag_enabled, set_show_float_pin_icon, set_show_titles, set_tab_from_top, set_tab_render_text,
set_ui_drag_threshold, set_tab_title_align, set_ui_drag_enabled, set_ui_drag_threshold,
status::{set_i3bar_separator, set_status, set_status_command, unset_status_command}, status::{set_i3bar_separator, set_status, set_status_command, unset_status_command},
switch_to_vt, switch_to_vt,
tasks::{self, JoinHandle}, tasks::{self, JoinHandle},
@ -1023,10 +1023,16 @@ impl State {
color!(HIGHLIGHT_COLOR, highlight_color); color!(HIGHLIGHT_COLOR, highlight_color);
color!(TAB_ACTIVE_BACKGROUND_COLOR, tab_active_bg_color); color!(TAB_ACTIVE_BACKGROUND_COLOR, tab_active_bg_color);
color!(TAB_ACTIVE_BORDER_COLOR, tab_active_border_color); color!(TAB_ACTIVE_BORDER_COLOR, tab_active_border_color);
color!(TAB_FOCUSED_BACKGROUND_COLOR, tab_focused_bg_color);
color!(TAB_FOCUSED_BORDER_COLOR, tab_focused_border_color);
color!(TAB_INACTIVE_BACKGROUND_COLOR, tab_inactive_bg_color); color!(TAB_INACTIVE_BACKGROUND_COLOR, tab_inactive_bg_color);
color!(TAB_INACTIVE_BORDER_COLOR, tab_inactive_border_color); color!(TAB_INACTIVE_BORDER_COLOR, tab_inactive_border_color);
color!(TAB_URGENT_BACKGROUND_COLOR, tab_urgent_bg_color);
color!(TAB_URGENT_BORDER_COLOR, tab_urgent_border_color);
color!(TAB_ACTIVE_TEXT_COLOR, tab_active_text_color); color!(TAB_ACTIVE_TEXT_COLOR, tab_active_text_color);
color!(TAB_FOCUSED_TEXT_COLOR, tab_focused_text_color);
color!(TAB_INACTIVE_TEXT_COLOR, tab_inactive_text_color); color!(TAB_INACTIVE_TEXT_COLOR, tab_inactive_text_color);
color!(TAB_URGENT_TEXT_COLOR, tab_urgent_text_color);
color!(TAB_BAR_BACKGROUND_COLOR, tab_bar_bg_color); color!(TAB_BAR_BACKGROUND_COLOR, tab_bar_bg_color);
color!(TAB_ATTENTION_BACKGROUND_COLOR, tab_attention_bg_color); color!(TAB_ATTENTION_BACKGROUND_COLOR, tab_attention_bg_color);
macro_rules! size { macro_rules! size {
@ -1048,6 +1054,7 @@ impl State {
size!(TAB_BAR_BORDER_WIDTH, tab_bar_border_width); size!(TAB_BAR_BORDER_WIDTH, tab_bar_border_width);
size!(TAB_BAR_TEXT_PADDING, tab_bar_text_padding); size!(TAB_BAR_TEXT_PADDING, tab_bar_text_padding);
size!(TAB_BAR_GAP, tab_bar_gap); size!(TAB_BAR_GAP, tab_bar_gap);
size!(TAB_OPACITY, tab_opacity);
macro_rules! font { macro_rules! font {
($fun:ident, $field:ident) => { ($fun:ident, $field:ident) => {
if let Some(font) = &theme.$field { if let Some(font) = &theme.$field {
@ -1064,6 +1071,12 @@ impl State {
if let Some(ref align) = theme.tab_title_align { if let Some(ref align) = theme.tab_title_align {
set_tab_title_align(align); set_tab_title_align(align);
} }
if let Some(from_top) = theme.tab_from_top {
set_tab_from_top(from_top);
}
if let Some(render_text) = theme.tab_render_text {
set_tab_render_text(render_text);
}
} }
fn handle_switch_device(self: &Rc<Self>, dev: InputDevice, actions: &Rc<SwitchActions>) { fn handle_switch_device(self: &Rc<Self>, dev: InputDevice, actions: &Rc<SwitchActions>) {