From 750bf06ce94cc9d9a45a4ca111e2309a1e2b8bbc Mon Sep 17 00:00:00 2001 From: atagen Date: Sun, 5 Apr 2026 20:04:13 +1000 Subject: [PATCH] add window gaps --- jay-config/src/theme.rs | 7 ++ src/config/handler.rs | 1 + src/renderer.rs | 86 ++++++++++++++++++++++++- src/theme.rs | 2 + src/tree/container.rs | 40 ++++++++---- src/tree/output.rs | 15 +++++ toml-config/src/config.rs | 1 + toml-config/src/config/parsers/theme.rs | 4 +- toml-config/src/lib.rs | 1 + 9 files changed, 144 insertions(+), 13 deletions(-) diff --git a/jay-config/src/theme.rs b/jay-config/src/theme.rs index 64883b09..134498d2 100644 --- a/jay-config/src/theme.rs +++ b/jay-config/src/theme.rs @@ -363,5 +363,12 @@ pub mod sized { /// /// Default: 1 const 04 => BAR_SEPARATOR_WIDTH, + /// The gap between tiled windows in pixels. + /// + /// When set to a value greater than 0, windows are separated by this + /// gap rather than by the border width. + /// + /// Default: 0 + const 05 => GAP, } } diff --git a/src/config/handler.rs b/src/config/handler.rs index 24f22015..435e3875 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -2440,6 +2440,7 @@ impl ConfigProxyHandler { BORDER_WIDTH => ThemeSized::border_width, BAR_HEIGHT => ThemeSized::bar_height, BAR_SEPARATOR_WIDTH => ThemeSized::bar_separator_width, + GAP => ThemeSized::gap, _ => return Err(CphError::UnknownSized(sized.0)), }; Ok(sized) diff --git a/src/renderer.rs b/src/renderer.rs index ca233cc0..3ef2e3da 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -334,18 +334,102 @@ impl Renderer<'_> { } } if let Some(child) = container.mono_child.get() { - let body = container.mono_body.get().move_(x, y); + let mb = container.mono_body.get(); + if self.state.theme.sizes.gap.get() > 0 && !child.node.node_is_container() { + let srgb_srgb = self.state.color_manager.srgb_gamma22(); + let bw = self.state.theme.sizes.border_width.get(); + let border_color = self.state.theme.colors.border.get(); + let focused_border_color = + self.state.theme.colors.focused_title_background.get(); + let c = if child.active.get() { + &focused_border_color + } else { + &border_color + }; + let full_h = mb.y2(); + let full_w = mb.width(); + let frame_rects = [ + Rect::new_sized_saturating(mb.x1() - bw, 0, bw, full_h), + Rect::new_sized_saturating(mb.x2(), 0, bw, full_h), + Rect::new_sized_saturating(mb.x1() - bw, -bw, full_w + 2 * bw, bw), + Rect::new_sized_saturating(mb.x1() - bw, full_h, full_w + 2 * bw, bw), + ]; + self.base.fill_boxes2( + &frame_rects, + c, + &srgb_srgb.linear, + RenderIntent::Perceptual, + x, + y, + ); + } + let body = mb.move_(x, y); let body = self.base.scale_rect(body); let content = container.mono_content.get(); child .node .node_render(self, x + content.x1(), y + content.y1(), Some(&body)); } else { + let gap = self.state.theme.sizes.gap.get(); + let (srgb_srgb, bw, border_color, focused_border_color, tpuh) = if gap > 0 { + let srgb_srgb = self.state.color_manager.srgb_gamma22(); + let bw = self.state.theme.sizes.border_width.get(); + let border_color = self.state.theme.colors.border.get(); + let focused_border_color = self.state.theme.colors.focused_title_background.get(); + let tpuh = self.state.theme.title_plus_underline_height(); + ( + Some(srgb_srgb), + bw, + border_color, + focused_border_color, + tpuh, + ) + } else { + (None, 0, Color::SOLID_BLACK, Color::SOLID_BLACK, 0) + }; for child in container.children.iter() { let body = child.body.get(); if body.x1() >= container.width.get() || body.y1() >= container.height.get() { break; } + if let Some(srgb_srgb) = srgb_srgb { + if !child.node.node_is_container() { + let srgb = &srgb_srgb.linear; + let c = if child.border_color_is_focused.get() { + &focused_border_color + } else { + &border_color + }; + let title_rect = child.title_rect.get(); + let top_y = if tpuh > 0 { title_rect.y1() } else { body.y1() }; + let full_h = body.y2() - top_y; + let full_w = body.width(); + let frame_rects = [ + Rect::new_sized_saturating(body.x1() - bw, top_y, bw, full_h), + Rect::new_sized_saturating(body.x2(), top_y, bw, full_h), + Rect::new_sized_saturating( + body.x1() - bw, + top_y - bw, + full_w + 2 * bw, + bw, + ), + Rect::new_sized_saturating( + body.x1() - bw, + body.y2(), + full_w + 2 * bw, + bw, + ), + ]; + self.base.fill_boxes2( + &frame_rects, + c, + srgb, + RenderIntent::Perceptual, + x, + y, + ); + } + } let body = body.move_(x, y); let body = self.base.scale_rect(body); let content = child.content.get(); diff --git a/src/theme.rs b/src/theme.rs index 303ed28a..2ee80489 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -586,6 +586,7 @@ sizes! { bar_height = (0, 1000, 17), border_width = (0, 1000, 4), bar_separator_width = (0, 1000, 1), + gap = (0, 1000, 0), } impl StaticText for ThemeSized { @@ -595,6 +596,7 @@ impl StaticText for ThemeSized { ThemeSized::bar_height => "Bar Height", ThemeSized::border_width => "Border Width", ThemeSized::bar_separator_width => "Bar Separator Width", + ThemeSized::gap => "Gap", } } } diff --git a/src/tree/container.rs b/src/tree/container.rs index e1c37924..231ce811 100644 --- a/src/tree/container.rs +++ b/src/tree/container.rs @@ -158,6 +158,7 @@ pub struct ContainerChild { pub body: Cell, pub content: Cell, factor: Cell, + pub border_color_is_focused: Cell, } #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] @@ -211,6 +212,7 @@ impl ContainerNode { title_rect: Default::default(), focus_history: Default::default(), attention_requested: Cell::new(false), + border_color_is_focused: Default::default(), }); let child_node_ref = child_node.clone(); let mut child_nodes = AHashMap::new(); @@ -332,6 +334,7 @@ impl ContainerNode { title_rect: Default::default(), focus_history: Default::default(), attention_requested: Default::default(), + border_color_is_focused: Default::default(), }); let r = link.to_ref(); links.insert(new.node_id(), link); @@ -382,11 +385,16 @@ impl ContainerNode { } fn damage(&self) { + let bw = if self.state.theme.sizes.gap.get() > 0 { + self.state.theme.sizes.border_width.get() + } else { + 0 + }; self.state.damage(Rect::new_sized_saturating( - self.abs_x1.get(), - self.abs_y1.get(), - self.width.get(), - self.height.get(), + self.abs_x1.get() - bw, + self.abs_y1.get() - bw, + self.width.get() + 2 * bw, + self.height.get() + 2 * bw, )); } @@ -447,6 +455,7 @@ impl ContainerNode { fn perform_split_layout(self: &Rc) { let sum_factors = self.sum_factors.get(); let border_width = self.state.theme.sizes.border_width.get(); + let spacing = self.state.theme.sizes.gap.get().max(border_width); let title_height_tmp = self.state.theme.title_height(); let title_plus_underline_height = self.state.theme.title_plus_underline_height(); let split = self.split.get(); @@ -482,7 +491,7 @@ impl ContainerNode { }; let body = Rect::new_sized_saturating(x1, y1, width, height); child.body.set(body); - pos += body_size + border_width; + pos += body_size + spacing; if split == ContainerSplit::Vertical { pos += title_plus_underline_height; } @@ -522,7 +531,7 @@ impl ContainerNode { }; body = Rect::new_sized_saturating(x1, y1, width, height); child.body.set(body); - pos += size + border_width; + pos += size + spacing; if split == ContainerSplit::Vertical { pos += title_plus_underline_height; } @@ -545,11 +554,12 @@ impl ContainerNode { fn update_content_size(&self) { let border_width = self.state.theme.sizes.border_width.get(); + let spacing = self.state.theme.sizes.gap.get().max(border_width); let title_plus_underline_height = self.state.theme.title_plus_underline_height(); let nc = self.num_children.get(); match self.split.get() { ContainerSplit::Horizontal => { - let new_content_size = self.width.get().sub((nc - 1) as i32 * border_width).max(0); + let new_content_size = self.width.get().sub((nc - 1) as i32 * spacing).max(0); self.content_width.set(new_content_size); self.content_height .set(self.height.get().sub(title_plus_underline_height).max(0)); @@ -560,7 +570,7 @@ impl ContainerNode { .get() .sub( title_plus_underline_height - + (nc - 1) as i32 * (border_width + title_plus_underline_height), + + (nc - 1) as i32 * (spacing + title_plus_underline_height), ) .max(0); self.content_height.set(new_content_size); @@ -832,6 +842,7 @@ impl ContainerNode { let have_active = self.children.iter().any(|c| c.active.get()); let abs_x = self.abs_x1.get(); let abs_y = self.abs_y1.get(); + let gap = self.state.theme.sizes.gap.get(); for (i, child) in self.children.iter().enumerate() { let rect = child.title_rect.get(); if self.toplevel_data.visible.get() && !mono && split != ContainerSplit::Horizontal { @@ -842,15 +853,17 @@ impl ContainerNode { rect.height() + tuh, )); } - if i > 0 { - let rect = if mono { + if gap > 0 && !mono && !child.node.node_is_container() { + child.border_color_is_focused.set(child.active.get()); + } else if gap == 0 && i > 0 { + let sep = if mono { Rect::new_sized_saturating(rect.x1() - bw, 0, bw, th) } else if split == ContainerSplit::Horizontal { Rect::new_sized_saturating(rect.x1() - bw, 0, bw, cheight) } else { Rect::new_sized_saturating(0, rect.y1() - bw, cwidth, bw) }; - rd.border_rects.push(rect); + rd.border_rects.push(sep); } if child.active.get() { rd.active_title_rects.push(rect); @@ -1184,6 +1197,9 @@ impl ContainerNode { // log::info!("node_child_active_changed"); self.schedule_render_titles(); self.schedule_compute_render_positions(); + if self.state.theme.sizes.gap.get() > 0 && self.toplevel_data.visible.get() { + self.damage(); + } if let Some(parent) = self.toplevel_data.parent.get() { parent.node_child_active_changed(self.deref(), active, depth + 1); } @@ -1920,6 +1936,7 @@ impl ContainingNode for ContainerNode { title_rect: Cell::new(node.title_rect.get()), focus_history: Cell::new(None), attention_requested: Cell::new(false), + border_color_is_focused: Default::default(), }); if let Some(fh) = node.focus_history.take() { link.focus_history.set(Some(fh.append(link.to_ref()))); @@ -1974,6 +1991,7 @@ impl ContainingNode for ContainerNode { }; let num_children = self.num_children.fetch_sub(1) - 1; if num_children == 0 { + self.damage(); self.tl_destroy(); return; } diff --git a/src/tree/output.rs b/src/tree/output.rs index 429741d5..80b524e8 100644 --- a/src/tree/output.rs +++ b/src/tree/output.rs @@ -819,6 +819,21 @@ impl OutputNode { .set(bar_rect_with_separator_rel); self.bar_separator_rect.set(bar_separator_rect); self.bar_separator_rect_rel.set(bar_separator_rect_rel); + let gap = self.state.theme.sizes.gap.get(); + if gap > 0 { + workspace_rect = Rect::new_sized_saturating( + workspace_rect.x1() + gap, + workspace_rect.y1() + gap, + (workspace_rect.width() - 2 * gap).max(0), + (workspace_rect.height() - 2 * gap).max(0), + ); + workspace_rect_rel = Rect::new_sized_saturating( + workspace_rect_rel.x1() + gap, + workspace_rect_rel.y1() + gap, + (workspace_rect_rel.width() - 2 * gap).max(0), + (workspace_rect_rel.height() - 2 * gap).max(0), + ); + } self.workspace_rect.set(workspace_rect); self.workspace_rect_rel.set(workspace_rect_rel); self.update_tray_positions(); diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 4359f8aa..e6edf19a 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -223,6 +223,7 @@ pub struct Theme { pub bar_font: Option, pub bar_position: Option, pub bar_separator_width: Option, + pub gap: Option, } #[derive(Debug, Clone)] diff --git a/toml-config/src/config/parsers/theme.rs b/toml-config/src/config/parsers/theme.rs index efcf6110..dae2674d 100644 --- a/toml-config/src/config/parsers/theme.rs +++ b/toml-config/src/config/parsers/theme.rs @@ -63,7 +63,7 @@ impl Parser for ThemeParser<'_> { font, title_font, ), - (bar_font, bar_position_val, bar_separator_width), + (bar_font, bar_position_val, bar_separator_width, gap), ) = ext.extract(( ( opt(val("attention-requested-bg-color")), @@ -93,6 +93,7 @@ impl Parser for ThemeParser<'_> { recover(opt(str("bar-font"))), recover(opt(str("bar-position"))), recover(opt(s32("bar-separator-width"))), + recover(opt(s32("gap"))), ), ))?; macro_rules! color { @@ -146,6 +147,7 @@ impl Parser for ThemeParser<'_> { bar_font: bar_font.map(|f| f.value.to_string()), bar_position, bar_separator_width: bar_separator_width.despan(), + gap: gap.despan(), }) } } diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 3a948cc5..6a70dba4 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -1003,6 +1003,7 @@ impl State { size!(TITLE_HEIGHT, title_height); size!(BAR_HEIGHT, bar_height); size!(BAR_SEPARATOR_WIDTH, bar_separator_width); + size!(GAP, gap); macro_rules! font { ($fun:ident, $field:ident) => { if let Some(font) = &theme.$field {