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:
parent
e35dce433a
commit
e8f86dae8a
28 changed files with 920 additions and 242 deletions
|
|
@ -2055,6 +2055,7 @@ impl ConfigClient {
|
|||
radius
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn seat_toggle_expand(&self, seat: Seat) {
|
||||
self.send(&ClientMessage::SeatToggleExpand { seat });
|
||||
}
|
||||
|
|
@ -2087,6 +2088,14 @@ impl ConfigClient {
|
|||
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) {
|
||||
self.send(&ClientMessage::SeatMoveTab { seat, right });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -911,6 +911,12 @@ pub enum ClientMessage<'a> {
|
|||
SetTabTitleAlign {
|
||||
align: u32,
|
||||
},
|
||||
SetTabFromTop {
|
||||
from_top: bool,
|
||||
},
|
||||
SetTabRenderText {
|
||||
render: bool,
|
||||
},
|
||||
SeatMoveTab {
|
||||
seat: Seat,
|
||||
right: bool,
|
||||
|
|
|
|||
|
|
@ -464,6 +464,21 @@ pub fn set_tab_title_align(align: &str) {
|
|||
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.
|
||||
///
|
||||
/// Only one callback can be set at a time. If another callback is already set, it will be
|
||||
|
|
|
|||
|
|
@ -334,6 +334,30 @@ pub mod colors {
|
|||
///
|
||||
/// Default: `#23092c`.
|
||||
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.
|
||||
|
|
@ -430,5 +454,9 @@ pub mod sized {
|
|||
///
|
||||
/// Default: 4
|
||||
const 12 => TAB_BAR_GAP,
|
||||
/// The opacity of tabs in the tab bar (0-100).
|
||||
///
|
||||
/// Default: 100
|
||||
const 13 => TAB_OPACITY,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -399,10 +399,10 @@ fn start_compositor2(
|
|||
hyprland_global_shortcuts: Default::default(),
|
||||
layer_rules: Default::default(),
|
||||
blur_config: Default::default(),
|
||||
blur_cache_epoch: Default::default(),
|
||||
animations_config: Default::default(),
|
||||
active_animations: Default::default(),
|
||||
close_snapshots: Default::default(),
|
||||
tab_animation_containers: Default::default(),
|
||||
});
|
||||
state.tracker.register(ClientId::from_raw(0));
|
||||
create_dummy_output(&state);
|
||||
|
|
|
|||
|
|
@ -2505,6 +2505,7 @@ impl ConfigProxyHandler {
|
|||
TAB_BAR_BORDER_WIDTH => ThemeSized::tab_bar_border_width,
|
||||
TAB_BAR_TEXT_PADDING => ThemeSized::tab_bar_text_padding,
|
||||
TAB_BAR_GAP => ThemeSized::tab_bar_gap,
|
||||
TAB_OPACITY => ThemeSized::tab_opacity,
|
||||
_ => return Err(CphError::UnknownSized(sized.0)),
|
||||
};
|
||||
Ok(sized)
|
||||
|
|
@ -2584,10 +2585,16 @@ impl ConfigProxyHandler {
|
|||
HIGHLIGHT_COLOR => ThemeColor::highlight,
|
||||
TAB_ACTIVE_BACKGROUND_COLOR => ThemeColor::tab_active_background,
|
||||
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_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_FOCUSED_TEXT_COLOR => ThemeColor::tab_focused_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_ATTENTION_BACKGROUND_COLOR => ThemeColor::tab_attention_background,
|
||||
_ => return Err(CphError::UnknownColor(colorable.0)),
|
||||
|
|
@ -3525,6 +3532,12 @@ impl ConfigProxyHandler {
|
|||
};
|
||||
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
|
||||
.handle_seat_move_tab(seat, right)
|
||||
.wrn("seat_move_tab")?,
|
||||
|
|
|
|||
|
|
@ -250,7 +250,8 @@ impl CursorUserGroup {
|
|||
}
|
||||
|
||||
fn damage(&self, rect: Rect) {
|
||||
self.state.damage2(true, self.hardware_cursor.get(), rect);
|
||||
self.state
|
||||
.damage2(true, self.hardware_cursor.get(), None, rect);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -116,6 +116,11 @@ pub struct BlurBackdrop {
|
|||
pub cache: Option<Rc<std::cell::RefCell<Option<BlurCacheEntry>>>>,
|
||||
pub cache_epoch: u64,
|
||||
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 {
|
||||
|
|
@ -363,16 +368,6 @@ pub struct RoundedFillRect {
|
|||
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 tex: Rc<dyn GfxTexture>,
|
||||
pub source: SampleRect,
|
||||
|
|
|
|||
|
|
@ -660,8 +660,8 @@ fn render_blur_backdrop(fb: &Framebuffer, b: &BlurBackdrop) {
|
|||
.mask
|
||||
.as_ref()
|
||||
.and_then(|m| m.texture.as_gl().map(|t| (m, t)));
|
||||
if let Some((mask, mask_tex_obj)) = mask_gl
|
||||
&& !mask_tex_obj.gl.external_only
|
||||
if b.corner_radius > 0.0
|
||||
|| 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).
|
||||
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.glUniform1i)(prog.tex, 0);
|
||||
(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_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_T, GL_CLAMP_TO_EDGE);
|
||||
(gles.glUniform1i)(prog.mask_tex, 1);
|
||||
(gles.glUniform1f)(prog.threshold, mask.threshold);
|
||||
let mask_tc = mask.source.to_points();
|
||||
(gles.glUniform1f)(
|
||||
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)(
|
||||
prog.texcoord as _,
|
||||
2,
|
||||
|
|
|
|||
|
|
@ -83,6 +83,9 @@ pub(crate) struct BlurCompositeProg {
|
|||
pub(crate) tex: GLint,
|
||||
pub(crate) mask_tex: GLint,
|
||||
pub(crate) threshold: GLint,
|
||||
pub(crate) corner_radius: GLint,
|
||||
pub(crate) pixel_size: GLint,
|
||||
pub(crate) mask_enabled: GLint,
|
||||
}
|
||||
|
||||
pub(crate) struct RoundedFillProg {
|
||||
|
|
@ -260,6 +263,9 @@ impl GlRenderContext {
|
|||
tex: prog.get_uniform_location(c"tex"),
|
||||
mask_tex: prog.get_uniform_location(c"mask_tex"),
|
||||
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,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,6 +5,20 @@ varying vec2 v_mask_texcoord;
|
|||
uniform sampler2D tex;
|
||||
uniform sampler2D mask_tex;
|
||||
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() {
|
||||
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)
|
||||
* 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 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ pub(super) struct BlurMaskRecord<'a> {
|
|||
pub(super) mask_source_points: [[f32; 2]; 4],
|
||||
pub(super) target_points: [[f32; 2]; 4],
|
||||
pub(super) threshold: f32,
|
||||
pub(super) corner_radius: f32,
|
||||
pub(super) pixel_size: [f32; 2],
|
||||
pub(super) _phantom: std::marker::PhantomData<&'a ()>,
|
||||
}
|
||||
|
||||
|
|
@ -145,6 +147,9 @@ impl VulkanRenderer {
|
|||
mask: Option<&BlurMaskRecord<'_>>,
|
||||
cached_blur: Option<&Rc<VulkanImage>>,
|
||||
out_blur_image: &mut Option<Rc<VulkanImage>>,
|
||||
corner_radius: f32,
|
||||
pixel_size: [f32; 2],
|
||||
target_points: [[f32; 2]; 4],
|
||||
) -> Result<(), VulkanError> {
|
||||
let [x1, y1, x2, y2] = rect;
|
||||
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.
|
||||
|
||||
if let Some(mask) = mask {
|
||||
if mask.is_some() || corner_radius > 0.0 {
|
||||
// Masked composite path: restore target to COLOR_ATTACHMENT and
|
||||
// draw the composite shader sampling levels[0] + mask.
|
||||
do_barriers(&[barrier(
|
||||
|
|
@ -464,18 +469,22 @@ impl VulkanRenderer {
|
|||
|
||||
// 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 mask_points = mask.map(|m| m.mask_source_points).unwrap_or(blurred_tc);
|
||||
let push = BlurCompositePushConstants {
|
||||
pos: mask.target_points,
|
||||
pos: mask.map(|m| m.target_points).unwrap_or(target_points),
|
||||
blurred_tex_pos: blurred_tc,
|
||||
mask_tex_pos: mask.mask_source_points,
|
||||
threshold: mask.threshold,
|
||||
mask_tex_pos: mask_points,
|
||||
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()
|
||||
.image_view(levels[0].texture_view)
|
||||
.image_layout(ImageLayout::SHADER_READ_ONLY_OPTIMAL);
|
||||
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);
|
||||
let writes = [
|
||||
WriteDescriptorSet::default()
|
||||
|
|
@ -671,6 +680,9 @@ impl VulkanRenderer {
|
|||
blurred_tex_pos: blurred_tc,
|
||||
mask_tex_pos: mask.mask_source_points,
|
||||
threshold: mask.threshold,
|
||||
corner_radius: mask.corner_radius,
|
||||
pixel_size: mask.pixel_size,
|
||||
mask_enabled: 1.0,
|
||||
};
|
||||
let blurred_image_info = DescriptorImageInfo::default()
|
||||
.image_view(cached.texture_view)
|
||||
|
|
|
|||
|
|
@ -27,16 +27,15 @@ use {
|
|||
semaphore::VulkanSemaphore,
|
||||
shaders::{
|
||||
BLUR_COMPOSITE_FRAG, BLUR_COMPOSITE_VERT, BLUR_DOWN_FRAG, BLUR_UP_FRAG, BLUR_VERT,
|
||||
BlurCompositePushConstants, BlurPushConstants,
|
||||
ColorManagementData, EotfArgs, FILL_FRAG, FILL_VERT, FillPushConstants,
|
||||
InvEotfArgs, LEGACY_FILL_FRAG, LEGACY_FILL_VERT, LEGACY_ROUNDED_FILL_FRAG,
|
||||
LEGACY_ROUNDED_FILL_VERT, LEGACY_ROUNDED_TEX_FRAG, LEGACY_ROUNDED_TEX_VERT,
|
||||
LEGACY_TEX_FRAG, LEGACY_TEX_VERT, LegacyFillPushConstants,
|
||||
LegacyRoundedFillPushConstants, LegacyRoundedTexPushConstants,
|
||||
LegacyTexPushConstants, OUT_FRAG, OUT_VERT, OutPushConstants, ROUNDED_FILL_FRAG,
|
||||
ROUNDED_FILL_VERT, ROUNDED_TEX_FRAG, ROUNDED_TEX_VERT, RoundedFillPushConstants,
|
||||
RoundedTexPushConstants, TEX_FRAG, TEX_VERT, TexPushConstants, TexVertex,
|
||||
VulkanShader,
|
||||
BlurCompositePushConstants, BlurPushConstants, ColorManagementData, EotfArgs,
|
||||
FILL_FRAG, FILL_VERT, FillPushConstants, InvEotfArgs, LEGACY_FILL_FRAG,
|
||||
LEGACY_FILL_VERT, LEGACY_ROUNDED_FILL_FRAG, LEGACY_ROUNDED_FILL_VERT,
|
||||
LEGACY_ROUNDED_TEX_FRAG, LEGACY_ROUNDED_TEX_VERT, LEGACY_TEX_FRAG, LEGACY_TEX_VERT,
|
||||
LegacyFillPushConstants, LegacyRoundedFillPushConstants,
|
||||
LegacyRoundedTexPushConstants, LegacyTexPushConstants, OUT_FRAG, OUT_VERT,
|
||||
OutPushConstants, ROUNDED_FILL_FRAG, ROUNDED_FILL_VERT, ROUNDED_TEX_FRAG,
|
||||
ROUNDED_TEX_VERT, RoundedFillPushConstants, RoundedTexPushConstants, TEX_FRAG,
|
||||
TEX_VERT, TexPushConstants, TexVertex, VulkanShader,
|
||||
},
|
||||
},
|
||||
io_uring::IoUring,
|
||||
|
|
@ -241,6 +240,8 @@ struct VulkanBlurOp {
|
|||
cache: Option<Rc<RefCell<Option<crate::gfx_api::BlurCacheEntry>>>>,
|
||||
cache_epoch: u64,
|
||||
cache_pixel_rect: [i32; 4],
|
||||
corner_radius: f32,
|
||||
pixel_size: [f32; 2],
|
||||
}
|
||||
|
||||
struct VulkanBlurMask {
|
||||
|
|
@ -1569,6 +1570,8 @@ impl VulkanRenderer {
|
|||
cache: b.cache.clone(),
|
||||
cache_epoch: b.cache_epoch,
|
||||
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(),
|
||||
target_points: blur.rect.to_points(),
|
||||
threshold: m.threshold,
|
||||
corner_radius: blur.corner_radius,
|
||||
pixel_size: blur.pixel_size,
|
||||
_phantom: std::marker::PhantomData,
|
||||
});
|
||||
|
||||
|
|
@ -2341,6 +2346,9 @@ impl VulkanRenderer {
|
|||
mask_record.as_ref(),
|
||||
cached_blur.as_ref(),
|
||||
&mut produced_blur,
|
||||
blur.corner_radius,
|
||||
blur.pixel_size,
|
||||
blur.rect.to_points(),
|
||||
)?;
|
||||
|
||||
// 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.
|
||||
(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) => {
|
||||
blur_rects.push(b.rect.to_rect(width, height));
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -198,6 +198,9 @@ pub struct BlurCompositePushConstants {
|
|||
pub blurred_tex_pos: [[f32; 2]; 4],
|
||||
pub mask_tex_pos: [[f32; 2]; 4],
|
||||
pub threshold: f32,
|
||||
pub corner_radius: f32,
|
||||
pub pixel_size: [f32; 2],
|
||||
pub mask_enabled: f32,
|
||||
}
|
||||
|
||||
unsafe impl Packed for BlurCompositePushConstants {}
|
||||
|
|
|
|||
|
|
@ -5,18 +5,33 @@ layout(set = 0, binding = 1) uniform sampler2D mask_tex;
|
|||
|
||||
layout(push_constant, std430) uniform Data {
|
||||
layout(offset = 96) float threshold;
|
||||
layout(offset = 100) float corner_radius;
|
||||
layout(offset = 104) vec2 pixel_size;
|
||||
layout(offset = 112) float mask_enabled;
|
||||
} data;
|
||||
|
||||
layout(location = 0) in vec2 v_blurred_tex_pos;
|
||||
layout(location = 1) in vec2 v_mask_tex_pos;
|
||||
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() {
|
||||
vec3 blurred = textureLod(blurred_tex, v_blurred_tex_pos, 0).rgb;
|
||||
vec2 uv = v_mask_tex_pos;
|
||||
float in_range = step(0.0, uv.x) * step(uv.x, 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 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);
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,6 +1,6 @@
|
|||
302a9f250bdc4f8e0e71a9f77c9a8a7aa55fd003bc91c2422a700c4abd83f54e src/gfx_apis/vulkan/shaders/alpha_modes.glsl
|
||||
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
|
||||
a04b2453c39efb018754fc25d45a369b5813359c55fad1c99020804cbb3a18e0 src/gfx_apis/vulkan/shaders/blur_down.frag
|
||||
f6d51f3b5410387d1474529c44e71bfdc31ceb80174ea6e3e4c2df30d03f11c3 src/gfx_apis/vulkan/shaders/blur_up.frag
|
||||
|
|
|
|||
|
|
@ -1494,7 +1494,7 @@ impl WlSurface {
|
|||
if let Some(tl) = self.toplevel.get() {
|
||||
damage = damage.intersect(tl.node_absolute_position());
|
||||
}
|
||||
self.client.state.damage(damage);
|
||||
self.client.state.damage_for_surface(self, damage);
|
||||
}
|
||||
}
|
||||
if self.visible.get() {
|
||||
|
|
@ -1520,7 +1520,7 @@ impl WlSurface {
|
|||
// Frame requests must be dispatched at the highest possible frame rate.
|
||||
// Therefore we must trigger a vsync of the output as soon as possible.
|
||||
let rect = output.global.pos.get();
|
||||
self.client.state.damage(rect);
|
||||
self.client.state.damage_for_surface(self, rect);
|
||||
}
|
||||
} else {
|
||||
if fifo_barrier_set {
|
||||
|
|
@ -1555,7 +1555,7 @@ impl WlSurface {
|
|||
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 pos = self.buffer_abs_pos.get();
|
||||
let apply_damage = |pos: Rect| {
|
||||
|
|
@ -1564,7 +1564,7 @@ impl WlSurface {
|
|||
if let Some(bounds) = bounds {
|
||||
damage = damage.intersect(bounds);
|
||||
}
|
||||
self.client.state.damage(damage);
|
||||
self.client.state.damage_for_surface(self, damage);
|
||||
} else {
|
||||
let matrix = self.damage_matrix.get();
|
||||
if let Some(buffer) = self.buffer.get() {
|
||||
|
|
@ -1577,7 +1577,7 @@ impl WlSurface {
|
|||
if let Some(bounds) = bounds {
|
||||
damage = damage.intersect(bounds);
|
||||
}
|
||||
self.client.state.damage(damage);
|
||||
self.client.state.damage_for_surface(self, damage);
|
||||
}
|
||||
}
|
||||
for damage in &pending.surface_damage {
|
||||
|
|
@ -1590,7 +1590,7 @@ impl WlSurface {
|
|||
damage = Rect::new_saturating(x1, y1, x2, y2);
|
||||
}
|
||||
damage = damage.intersect(bounds.unwrap_or(pos));
|
||||
self.client.state.damage(damage);
|
||||
self.client.state.damage_for_surface(self, damage);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -246,7 +246,6 @@ where
|
|||
dx * dx + dy * dy
|
||||
}
|
||||
|
||||
#[expect(dead_code)]
|
||||
pub fn contains_rect<U>(&self, rect: &Rect<U>) -> bool
|
||||
where
|
||||
U: Tag,
|
||||
|
|
|
|||
223
src/renderer.rs
223
src/renderer.rs
|
|
@ -346,7 +346,7 @@ impl Renderer<'_> {
|
|||
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_srgb.linear;
|
||||
let perceptual = RenderIntent::Perceptual;
|
||||
|
|
@ -355,100 +355,160 @@ impl Renderer<'_> {
|
|||
let radius = self.state.theme.sizes.tab_bar_radius.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 padding = self.state.theme.sizes.tab_bar_padding.get();
|
||||
let bar_height = tab_bar.height;
|
||||
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).
|
||||
// We use:
|
||||
// FillRect – tiny strip for Vulkan paint regions (hidden)
|
||||
// RoundedFillRect z0 – solid rounded bg
|
||||
// RoundedFillRect z1 – rounded border ring (on top of bg)
|
||||
// RoundedCopyTexture – title text (on top of everything)
|
||||
for entry in &tab_bar.entries {
|
||||
let (bg_color, border_color, _text_color) = TabBar::entry_colors(self.state, entry);
|
||||
let ex = entry.x.get();
|
||||
let ew = entry.width.get();
|
||||
let tab_rect = Rect::new_sized_saturating(ex, 0, ew, bar_height);
|
||||
let tab_cr = CornerRadius::from(radius as f32);
|
||||
// Compute static positions for non-destroying entries.
|
||||
let active_entries: Vec<usize> = tab_bar
|
||||
.entries
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, e)| !e.destroying)
|
||||
.map(|(i, _)| i)
|
||||
.collect();
|
||||
let n = active_entries.len() as i32;
|
||||
if n == 0 {
|
||||
return;
|
||||
}
|
||||
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
|
||||
// behind the RoundedFillRect bg that renders later).
|
||||
let strip = Rect::new_sized_saturating(
|
||||
ex + radius,
|
||||
bar_height / 2,
|
||||
(ew - 2 * radius).max(1),
|
||||
1,
|
||||
);
|
||||
self.base
|
||||
.fill_boxes2(slice::from_ref(&strip), &bg_color, srgb, perceptual, x, y);
|
||||
// Build a map: entry index → (x, width) in pixels.
|
||||
let mut positions: Vec<(i32, i32)> = vec![(0, 0); tab_bar.entries.len()];
|
||||
let mut tab_x = padding;
|
||||
for &idx in &active_entries {
|
||||
let w = if remainder > 0 {
|
||||
remainder -= 1;
|
||||
per_tab + 1
|
||||
} else {
|
||||
per_tab
|
||||
};
|
||||
positions[idx] = (tab_x, w);
|
||||
tab_x += w + padding;
|
||||
}
|
||||
|
||||
// Rounded solid bg fill (z_order=0, renders first among RoundedFill).
|
||||
self.base.fill_rounded_rect_z(
|
||||
tab_rect.move_(x, y),
|
||||
&bg_color,
|
||||
None,
|
||||
srgb,
|
||||
perceptual,
|
||||
tab_cr.scaled_by(scalef),
|
||||
0.0,
|
||||
0,
|
||||
);
|
||||
let render_entry =
|
||||
|renderer: &mut Self, idx: usize, entry: &crate::tree::tab_bar::TabBarEntry| {
|
||||
let fade = entry.fade_opacity.value() * tab_opacity;
|
||||
if fade < 1e-4 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Rounded border ring on top (z_order=1, renders after bg).
|
||||
if border_width > 0 {
|
||||
self.base.fill_rounded_rect_z(
|
||||
let (ex, ew) = positions[idx];
|
||||
if ew < 1 {
|
||||
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),
|
||||
&border_color,
|
||||
&bg_color,
|
||||
None,
|
||||
srgb,
|
||||
perceptual,
|
||||
tab_cr.scaled_by(scalef),
|
||||
border_width as f32 * scalef,
|
||||
1,
|
||||
0.0,
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
// Title text as RoundedCopyTexture (sorts after all RoundedFill).
|
||||
let tex_ref = entry.title_texture.borrow();
|
||||
if let Some(tex) = tex_ref.as_ref()
|
||||
&& let Some(texture) = tex.texture()
|
||||
{
|
||||
use crate::theme::TabTitleAlign;
|
||||
let (tw, _th) = texture.size();
|
||||
let tex_width = (tw as f64 / render_scale.to_f64()).round() as i32;
|
||||
let tab_inner = ew - 2 * (text_padding + border_width);
|
||||
let text_x = match self.state.theme.tab_title_align.get() {
|
||||
TabTitleAlign::Start => x + ex + text_padding + border_width,
|
||||
TabTitleAlign::Center => {
|
||||
x + ex
|
||||
+ border_width
|
||||
+ (tab_inner.max(0) - tex_width).max(0) / 2
|
||||
+ text_padding.min(tab_inner.max(0) / 2)
|
||||
}
|
||||
TabTitleAlign::End => {
|
||||
let end_x = x + ex + ew - tex_width - text_padding - border_width;
|
||||
end_x.max(x + ex + border_width)
|
||||
}
|
||||
};
|
||||
let (tx, ty) = self.base.scale_point(text_x, y);
|
||||
self.base.render_rounded_texture(
|
||||
&texture,
|
||||
None,
|
||||
tx,
|
||||
ty,
|
||||
None,
|
||||
None,
|
||||
render_scale,
|
||||
None,
|
||||
None,
|
||||
AcquireSync::None,
|
||||
ReleaseSync::None,
|
||||
self.state.color_manager.srgb_gamma22(),
|
||||
perceptual,
|
||||
AlphaMode::PremultipliedElectrical,
|
||||
CornerRadius::from(0.0_f32),
|
||||
);
|
||||
if border_width > 0 {
|
||||
renderer.base.fill_rounded_rect_z(
|
||||
tab_rect.move_(x, y),
|
||||
&border_color,
|
||||
None,
|
||||
srgb,
|
||||
perceptual,
|
||||
tab_cr.scaled_by(scalef),
|
||||
border_width as f32 * scalef,
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
if !render_text {
|
||||
return;
|
||||
}
|
||||
|
||||
let tex_ref = entry.title_texture.borrow();
|
||||
if let Some(tex) = tex_ref.as_ref()
|
||||
&& let Some(texture) = tex.texture()
|
||||
{
|
||||
use crate::theme::TabTitleAlign;
|
||||
let (tw, _th) = texture.size();
|
||||
let tex_width = (tw as f64 / render_scale.to_f64()).round() as i32;
|
||||
let tab_inner = ew - 2 * (text_padding + border_width);
|
||||
let text_x = match renderer.state.theme.tab_title_align.get() {
|
||||
TabTitleAlign::Start => x + ex + text_padding + border_width,
|
||||
TabTitleAlign::Center => {
|
||||
x + ex
|
||||
+ border_width
|
||||
+ (tab_inner.max(0) - tex_width).max(0) / 2
|
||||
+ text_padding.min(tab_inner.max(0) / 2)
|
||||
}
|
||||
TabTitleAlign::End => {
|
||||
let end_x = x + ex + ew - tex_width - text_padding - border_width;
|
||||
end_x.max(x + ex + border_width)
|
||||
}
|
||||
};
|
||||
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);
|
||||
|
||||
if let Some(child) = container.mono_child.get() {
|
||||
// Render tab bar if present.
|
||||
{
|
||||
let tab_bar = container.tab_bar.borrow();
|
||||
if let Some(tb) = tab_bar.as_ref() {
|
||||
|
|
|
|||
|
|
@ -411,6 +411,19 @@ impl RendererBase<'_> {
|
|||
mask: Option<BlurMask>,
|
||||
cache: Option<Rc<std::cell::RefCell<Option<crate::gfx_api::BlurCacheEntry>>>>,
|
||||
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(
|
||||
rect.x1() as f32,
|
||||
|
|
@ -422,6 +435,10 @@ impl RendererBase<'_> {
|
|||
self.fb_height,
|
||||
);
|
||||
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 {
|
||||
rect: target,
|
||||
passes,
|
||||
|
|
@ -430,6 +447,8 @@ impl RendererBase<'_> {
|
|||
cache,
|
||||
cache_epoch,
|
||||
cache_pixel_rect,
|
||||
corner_radius,
|
||||
pixel_size,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
66
src/state.rs
66
src/state.rs
|
|
@ -306,10 +306,10 @@ pub struct State {
|
|||
pub hyprland_global_shortcuts: CopyHashMap<(String, String), Rc<HyprlandGlobalShortcutV1>>,
|
||||
pub layer_rules: RefCell<Vec<jay_config::_private::LayerRuleIpc>>,
|
||||
pub blur_config: Cell<jay_config::_private::BlurConfigIpc>,
|
||||
pub blur_cache_epoch: NumCell<u64>,
|
||||
pub animations_config: Cell<jay_config::_private::AnimationsConfigIpc>,
|
||||
pub active_animations: RefCell<Vec<std::rc::Weak<dyn ToplevelNode>>>,
|
||||
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 {
|
||||
|
|
@ -1046,10 +1046,25 @@ impl State {
|
|||
}
|
||||
|
||||
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() {
|
||||
return;
|
||||
}
|
||||
|
|
@ -1059,11 +1074,18 @@ impl State {
|
|||
for surface in layer.iter() {
|
||||
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() {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
|
@ -1518,23 +1540,31 @@ impl State {
|
|||
}
|
||||
}
|
||||
let mut snapshots = self.close_snapshots.borrow_mut();
|
||||
if snapshots.is_empty() {
|
||||
return;
|
||||
}
|
||||
snapshots.retain(|snap| {
|
||||
if snap.close_progress(self).is_none() {
|
||||
// Final damage so the snapshot's last-rendered position gets
|
||||
// repainted (clearing any leftover pixels).
|
||||
if !snapshots.is_empty() {
|
||||
snapshots.retain(|snap| {
|
||||
if snap.close_progress(self).is_none() {
|
||||
if let Some(output) = snap.output.upgrade() {
|
||||
self.damage(output.global.pos.get());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if let Some(output) = snap.output.upgrade() {
|
||||
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) {
|
||||
|
|
|
|||
26
src/theme.rs
26
src/theme.rs
|
|
@ -183,6 +183,14 @@ impl Color {
|
|||
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 {
|
||||
Self::from_srgb(g, g, g)
|
||||
}
|
||||
|
|
@ -456,10 +464,16 @@ colors! {
|
|||
highlight = (0x9d, 0x28, 0xc6, 0x7f),
|
||||
tab_active_background = (0x4c, 0x78, 0x99),
|
||||
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_border = (0x33, 0x33, 0x33),
|
||||
tab_urgent_background = (0x23, 0x09, 0x2c),
|
||||
tab_urgent_border = (0x44, 0x11, 0x55),
|
||||
tab_active_text = (0xff, 0xff, 0xff),
|
||||
tab_focused_text = (0xdd, 0xdd, 0xdd),
|
||||
tab_inactive_text = (0x88, 0x88, 0x88),
|
||||
tab_urgent_text = (0xff, 0xff, 0xff),
|
||||
tab_bar_background = (0x00, 0x00, 0x00, 0x00),
|
||||
tab_attention_background = (0x23, 0x09, 0x2c),
|
||||
}
|
||||
|
|
@ -486,10 +500,16 @@ impl StaticText for ThemeColor {
|
|||
ThemeColor::highlight => "Highlight",
|
||||
ThemeColor::tab_active_background => "Tab Background (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_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_focused_text => "Tab Text (focused)",
|
||||
ThemeColor::tab_inactive_text => "Tab Text (inactive)",
|
||||
ThemeColor::tab_urgent_text => "Tab Text (urgent)",
|
||||
ThemeColor::tab_bar_background => "Tab Bar Background",
|
||||
ThemeColor::tab_attention_background => "Tab Attention Background",
|
||||
}
|
||||
|
|
@ -610,6 +630,7 @@ sizes! {
|
|||
tab_bar_border_width = (0, 1000, 2),
|
||||
tab_bar_text_padding = (0, 1000, 4),
|
||||
tab_bar_gap = (0, 1000, 4),
|
||||
tab_opacity = (0, 100, 100),
|
||||
}
|
||||
|
||||
impl StaticText for ThemeSized {
|
||||
|
|
@ -627,6 +648,7 @@ impl StaticText for ThemeSized {
|
|||
ThemeSized::tab_bar_border_width => "Tab Bar Border Width",
|
||||
ThemeSized::tab_bar_text_padding => "Tab Bar Text Padding",
|
||||
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 autotile_enabled: Cell<bool>,
|
||||
pub tab_title_align: Cell<TabTitleAlign>,
|
||||
pub tab_from_top: Cell<bool>,
|
||||
pub tab_render_text: Cell<bool>,
|
||||
}
|
||||
|
||||
impl Default for Theme {
|
||||
|
|
@ -802,6 +826,8 @@ impl Default for Theme {
|
|||
corner_radius: Cell::new(CornerRadius::default()),
|
||||
autotile_enabled: Cell::new(false),
|
||||
tab_title_align: Cell::new(TabTitleAlign::default()),
|
||||
tab_from_top: Cell::new(false),
|
||||
tab_render_text: Cell::new(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,10 +18,7 @@ use {
|
|||
ContainingNode, Direction, FindTreeResult, FindTreeUsecase, FloatNode, FoundNode, Node,
|
||||
NodeId, NodeLayerLink, NodeLocation, OutputNode, TddType, TileDragDestination,
|
||||
ToplevelData, ToplevelNode, ToplevelNodeBase, ToplevelType, WorkspaceNode,
|
||||
default_tile_drag_bounds,
|
||||
tab_bar::{TabBar, TabBarEntry},
|
||||
toplevel_set_workspace,
|
||||
walker::NodeVisitor,
|
||||
default_tile_drag_bounds, tab_bar::TabBar, toplevel_set_workspace, walker::NodeVisitor,
|
||||
},
|
||||
utils::{
|
||||
clonecell::CloneCell,
|
||||
|
|
@ -265,6 +262,9 @@ impl ContainerNode {
|
|||
update_tab_textures_scheduled: Cell::new(false),
|
||||
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());
|
||||
slf.pull_child_properties(&child_node_ref);
|
||||
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) {
|
||||
self.compute_render_positions_scheduled.set(false);
|
||||
let mut rd = self.render_data.borrow_mut();
|
||||
|
|
@ -780,6 +806,7 @@ impl ContainerNode {
|
|||
if self.mono_child.is_some() == child.is_some() {
|
||||
return;
|
||||
}
|
||||
let exiting_mono = self.mono_child.is_some() && child.is_none();
|
||||
let child = {
|
||||
let children = self.child_nodes.borrow();
|
||||
match child {
|
||||
|
|
@ -813,6 +840,9 @@ impl ContainerNode {
|
|||
} else {
|
||||
for child in self.children.iter() {
|
||||
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()
|
||||
}
|
||||
|
||||
/// Rebuild the tab bar. If `override_id` and `override_title` are provided,
|
||||
/// use the override title for that child instead of borrowing it (avoids
|
||||
/// RefCell double-borrow when called from node_child_title_changed).
|
||||
/// Update the tab bar entries to match the current children. Preserves
|
||||
/// existing entries (and their animation state) for children still present,
|
||||
/// starts exit animations for removed children, and creates new entries
|
||||
/// with enter animations for added children.
|
||||
fn rebuild_tab_bar_with_override(
|
||||
self: &Rc<Self>,
|
||||
override_id: Option<NodeId>,
|
||||
|
|
@ -938,24 +969,35 @@ impl ContainerNode {
|
|||
.persistent
|
||||
.scale
|
||||
.get();
|
||||
let mut bar = TabBar::new(height, render_scale);
|
||||
for child in self.children.iter() {
|
||||
let child_id = child.node.node_id();
|
||||
let title = self.get_child_tab_title(&child, override_id, override_title);
|
||||
bar.entries.push(TabBarEntry {
|
||||
child_id,
|
||||
title,
|
||||
title_texture: Rc::new(RefCell::new(None)),
|
||||
active: child_id == active_id,
|
||||
attention_requested: child.attention_requested.get(),
|
||||
x: Cell::new(0),
|
||||
width: Cell::new(0),
|
||||
});
|
||||
}
|
||||
let padding = self.state.theme.sizes.tab_bar_padding.get();
|
||||
bar.layout_entries(self.width.get(), padding);
|
||||
*self.tab_bar.borrow_mut() = Some(bar);
|
||||
let now = self.state.now_nsec();
|
||||
|
||||
let children_data: Vec<_> = self
|
||||
.children
|
||||
.iter()
|
||||
.map(|child| {
|
||||
let child_id = child.node.node_id();
|
||||
let title = self.get_child_tab_title(&child, override_id, override_title);
|
||||
let active = child_id == active_id;
|
||||
let focused = active && child.active.get();
|
||||
let urgent = child.attention_requested.get();
|
||||
(child_id, title, active, focused, urgent)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut bar_ref = self.tab_bar.borrow_mut();
|
||||
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();
|
||||
|
||||
if animating {
|
||||
self.register_tab_animation();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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();
|
||||
if let Some(tb) = tab_bar.as_ref() {
|
||||
if seat_data.y >= 0 && seat_data.y < tb.height {
|
||||
if let Some(idx) = tb.entry_at_x(seat_data.x) {
|
||||
let child_id = tb.entries[idx].child_id;
|
||||
let padding = self.state.theme.sizes.tab_bar_padding.get();
|
||||
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(seat_datas);
|
||||
let children = self.child_nodes.borrow();
|
||||
|
|
@ -1698,6 +1743,7 @@ impl ContainerNode {
|
|||
let entries: Vec<_> = tb
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|e| !e.destroying)
|
||||
.map(|e| {
|
||||
(
|
||||
e.title.clone(),
|
||||
|
|
@ -1722,7 +1768,6 @@ impl ContainerNode {
|
|||
if let Some(s) = scale {
|
||||
texture_height = (bar_height as f64 * s).round() as _;
|
||||
}
|
||||
let mut scheduled = 0;
|
||||
let mut texture_refs = Vec::new();
|
||||
for (title, (_, _, text_color), title_texture) in &entries {
|
||||
let mut tex_ref = title_texture.borrow_mut();
|
||||
|
|
@ -1737,7 +1782,6 @@ impl ContainerNode {
|
|||
scale,
|
||||
);
|
||||
texture_refs.push(title_texture.clone());
|
||||
scheduled += 1;
|
||||
}
|
||||
(on_completed.event(), texture_refs)
|
||||
}
|
||||
|
|
@ -2404,11 +2448,9 @@ impl ToplevelNodeBase for ContainerNode {
|
|||
size_changed |= self.height.replace(rect.height()) != rect.height();
|
||||
if size_changed {
|
||||
self.update_content_size();
|
||||
// Re-layout tab bar entries when container size changes in mono mode.
|
||||
if self.mono_child.is_some() {
|
||||
if let Some(bar) = self.tab_bar.borrow().as_ref() {
|
||||
let padding = self.state.theme.sizes.tab_bar_padding.get();
|
||||
bar.layout_entries(rect.width(), padding);
|
||||
if let Some(bar) = self.tab_bar.borrow_mut().as_mut() {
|
||||
bar.update_animations();
|
||||
}
|
||||
}
|
||||
// log::info!("tl_change_extents");
|
||||
|
|
|
|||
|
|
@ -1,11 +1,85 @@
|
|||
use {
|
||||
crate::{scale::Scale, state::State, text::TextTexture, theme::Color, tree::NodeId},
|
||||
std::{
|
||||
cell::{Cell, RefCell},
|
||||
rc::Rc,
|
||||
crate::{
|
||||
scale::Scale,
|
||||
state::State,
|
||||
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.
|
||||
pub struct TabBarEntry {
|
||||
/// The node ID of the child this tab represents.
|
||||
|
|
@ -14,19 +88,185 @@ pub struct TabBarEntry {
|
|||
pub title: String,
|
||||
/// Pre-rendered text texture for the tab title.
|
||||
pub title_texture: Rc<RefCell<Option<TextTexture>>>,
|
||||
/// Whether this is the active (visible) tab.
|
||||
pub active: bool,
|
||||
/// Whether this tab's window has requested attention.
|
||||
pub attention_requested: bool,
|
||||
/// X offset of this tab within the tab bar (relative to tab bar start).
|
||||
pub x: Cell<i32>,
|
||||
/// Width of this tab in pixels.
|
||||
pub width: Cell<i32>,
|
||||
/// Whether this entry is being destroyed (exit animation in flight).
|
||||
pub destroying: bool,
|
||||
|
||||
/// Horizontal offset as a fraction of total bar width (0.0–1.0).
|
||||
pub offset: AnimatedF32,
|
||||
/// Width as a fraction of total bar width (0.0–1.0).
|
||||
pub width: AnimatedF32,
|
||||
/// 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.0–1.0.
|
||||
pub active_anim: AnimatedF32,
|
||||
/// Focused (keyboard focus on this tab's container) blend weight: 0.0–1.0.
|
||||
pub focused_anim: AnimatedF32,
|
||||
/// Urgent/attention blend weight: 0.0–1.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.
|
||||
pub struct TabBar {
|
||||
/// The individual tab entries.
|
||||
/// The individual tab entries (persistent across updates).
|
||||
pub entries: Vec<TabBarEntry>,
|
||||
/// Height of the tab bar in pixels (from theme).
|
||||
pub height: i32,
|
||||
|
|
@ -35,7 +275,6 @@ pub struct TabBar {
|
|||
}
|
||||
|
||||
impl TabBar {
|
||||
/// Create a new empty tab bar.
|
||||
pub fn new(height: i32, render_scale: Scale) -> Self {
|
||||
Self {
|
||||
entries: Vec::new(),
|
||||
|
|
@ -44,67 +283,116 @@ impl TabBar {
|
|||
}
|
||||
}
|
||||
|
||||
/// Recompute the positions and widths of all tab entries.
|
||||
///
|
||||
/// `total_width` is the available width for the entire tab bar.
|
||||
pub fn layout_entries(&self, total_width: i32, padding: i32) {
|
||||
let n = self.entries.len() as i32;
|
||||
if n == 0 {
|
||||
/// Update entries to match the current set of children. Preserves existing
|
||||
/// entries (and their animation state) for children that are still present,
|
||||
/// starts exit animations for removed children, and creates new entries with
|
||||
/// enter animations for added children.
|
||||
pub fn update_entries(
|
||||
&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;
|
||||
}
|
||||
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 available = (total_width - total_padding).max(0);
|
||||
let per_tab = available / n;
|
||||
let mut remainder = available - per_tab * n;
|
||||
let mut x = padding;
|
||||
for entry in &self.entries {
|
||||
let mut tab_x = padding;
|
||||
for (i, (_idx, _entry)) in active_entries.iter().enumerate() {
|
||||
let w = if remainder > 0 {
|
||||
remainder -= 1;
|
||||
per_tab + 1
|
||||
} else {
|
||||
per_tab
|
||||
};
|
||||
entry.x.set(x);
|
||||
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 {
|
||||
if x >= tab_x && x < tab_x + w {
|
||||
return Some(i);
|
||||
}
|
||||
tab_x += w + padding;
|
||||
}
|
||||
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) {
|
||||
let theme = &state.theme;
|
||||
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(),
|
||||
)
|
||||
}
|
||||
entry.blended_colors(state)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -241,10 +241,16 @@ pub struct Theme {
|
|||
pub corner_radius: Option<f32>,
|
||||
pub tab_active_bg_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_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_focused_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_attention_bg_color: Option<Color>,
|
||||
pub tab_bar_height: Option<i32>,
|
||||
|
|
@ -253,7 +259,10 @@ pub struct Theme {
|
|||
pub tab_bar_border_width: Option<i32>,
|
||||
pub tab_bar_text_padding: Option<i32>,
|
||||
pub tab_bar_gap: Option<i32>,
|
||||
pub tab_opacity: Option<i32>,
|
||||
pub tab_title_align: Option<String>,
|
||||
pub tab_from_top: Option<bool>,
|
||||
pub tab_render_text: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -398,6 +407,7 @@ pub enum AnimationCurve {
|
|||
Linear,
|
||||
EaseOut,
|
||||
EaseInOut,
|
||||
#[allow(dead_code)]
|
||||
Bezier { x1: f32, y1: f32, x2: f32, y2: f32 },
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -105,41 +105,58 @@ impl Parser for ThemeParser<'_> {
|
|||
(
|
||||
tab_active_bg_color,
|
||||
tab_active_border_color,
|
||||
tab_focused_bg_color,
|
||||
tab_focused_border_color,
|
||||
tab_inactive_bg_color,
|
||||
tab_inactive_border_color,
|
||||
tab_urgent_bg_color,
|
||||
tab_urgent_border_color,
|
||||
tab_active_text_color,
|
||||
tab_focused_text_color,
|
||||
),
|
||||
(
|
||||
tab_inactive_text_color,
|
||||
tab_urgent_text_color,
|
||||
tab_bar_bg_color,
|
||||
tab_attention_bg_color,
|
||||
tab_bar_height,
|
||||
tab_bar_padding,
|
||||
),
|
||||
(
|
||||
tab_bar_radius,
|
||||
tab_bar_border_width,
|
||||
tab_bar_text_padding,
|
||||
tab_bar_gap,
|
||||
tab_title_align_val,
|
||||
),
|
||||
(tab_opacity, tab_title_align_val, tab_from_top, tab_render_text),
|
||||
) = ext.extract((
|
||||
(
|
||||
opt(val("tab-active-bg-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-border-color")),
|
||||
opt(val("tab-urgent-bg-color")),
|
||||
opt(val("tab-urgent-border-color")),
|
||||
opt(val("tab-active-text-color")),
|
||||
opt(val("tab-focused-text-color")),
|
||||
),
|
||||
(
|
||||
opt(val("tab-inactive-text-color")),
|
||||
opt(val("tab-urgent-text-color")),
|
||||
opt(val("tab-bar-bg-color")),
|
||||
opt(val("tab-attention-bg-color")),
|
||||
recover(opt(s32("tab-bar-height"))),
|
||||
recover(opt(s32("tab-bar-padding"))),
|
||||
),
|
||||
(
|
||||
recover(opt(s32("tab-bar-radius"))),
|
||||
recover(opt(s32("tab-bar-border-width"))),
|
||||
recover(opt(s32("tab-bar-text-padding"))),
|
||||
recover(opt(s32("tab-bar-gap"))),
|
||||
),
|
||||
(
|
||||
recover(opt(s32("tab-opacity"))),
|
||||
recover(opt(str("tab-title-align"))),
|
||||
recover(opt(bol("tab-from-top"))),
|
||||
recover(opt(bol("tab-render-text"))),
|
||||
),
|
||||
))?;
|
||||
macro_rules! color {
|
||||
|
|
@ -199,10 +216,16 @@ impl Parser for ThemeParser<'_> {
|
|||
corner_radius: corner_radius.map(|v| v.value as f32),
|
||||
tab_active_bg_color: color!(tab_active_bg_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_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_focused_text_color: color!(tab_focused_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_attention_bg_color: color!(tab_attention_bg_color),
|
||||
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_text_padding: tab_bar_text_padding.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_from_top: tab_from_top.despan(),
|
||||
tab_render_text: tab_render_text.despan(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ use {
|
|||
crate::{
|
||||
config::{
|
||||
Action, AnimationCurve, AnimationsConfig, BlurConfig, ClientRule, Config,
|
||||
ConfigConnector, ConfigDrmDevice, ConfigKeymap,
|
||||
ConnectorMatch, DrmDeviceMatch, Exec, Input, InputMatch, LayerKind, LayerRule, Output,
|
||||
OutputMatch, SimpleCommand, Status, Theme, WindowRule, parse_config,
|
||||
ConfigConnector, ConfigDrmDevice, ConfigKeymap, ConnectorMatch, DrmDeviceMatch, Exec,
|
||||
Input, InputMatch, LayerKind, LayerRule, Output, OutputMatch, SimpleCommand, Status,
|
||||
Theme, WindowRule, parse_config,
|
||||
},
|
||||
rules::{MatcherTemp, RuleMapper},
|
||||
shortcuts::ModeState,
|
||||
|
|
@ -47,8 +47,8 @@ use {
|
|||
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_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_ui_drag_threshold,
|
||||
set_show_float_pin_icon, set_show_titles, set_tab_from_top, set_tab_render_text,
|
||||
set_tab_title_align, set_ui_drag_enabled, set_ui_drag_threshold,
|
||||
status::{set_i3bar_separator, set_status, set_status_command, unset_status_command},
|
||||
switch_to_vt,
|
||||
tasks::{self, JoinHandle},
|
||||
|
|
@ -1023,10 +1023,16 @@ impl State {
|
|||
color!(HIGHLIGHT_COLOR, highlight_color);
|
||||
color!(TAB_ACTIVE_BACKGROUND_COLOR, tab_active_bg_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_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_FOCUSED_TEXT_COLOR, tab_focused_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_ATTENTION_BACKGROUND_COLOR, tab_attention_bg_color);
|
||||
macro_rules! size {
|
||||
|
|
@ -1048,6 +1054,7 @@ impl State {
|
|||
size!(TAB_BAR_BORDER_WIDTH, tab_bar_border_width);
|
||||
size!(TAB_BAR_TEXT_PADDING, tab_bar_text_padding);
|
||||
size!(TAB_BAR_GAP, tab_bar_gap);
|
||||
size!(TAB_OPACITY, tab_opacity);
|
||||
macro_rules! font {
|
||||
($fun:ident, $field:ident) => {
|
||||
if let Some(font) = &theme.$field {
|
||||
|
|
@ -1064,6 +1071,12 @@ impl State {
|
|||
if let Some(ref align) = theme.tab_title_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>) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue