From a6e629dd2f5f3d9b6980cd3b9acddbea9baf0a5d Mon Sep 17 00:00:00 2001 From: Stipe Kotarac Date: Mon, 1 Dec 2025 18:15:11 +0100 Subject: [PATCH] theme: add bar-separator-width setting --- jay-config/src/theme.rs | 6 ++- src/compositor.rs | 4 +- src/config/handler.rs | 1 + src/it/test_config.rs | 22 ++++++++ src/it/tests.rs | 2 + src/it/tests/t0052_bar.rs | 67 +++++++++++++++++++++++++ src/renderer.rs | 2 +- src/tasks/connector.rs | 4 +- src/theme.rs | 5 ++ src/tree/output.rs | 54 +++++++++++--------- toml-config/src/config.rs | 1 + toml-config/src/config/parsers/theme.rs | 4 +- toml-config/src/lib.rs | 1 + toml-spec/spec/spec.generated.json | 5 ++ toml-spec/spec/spec.generated.md | 10 ++++ toml-spec/spec/spec.yaml | 6 +++ 16 files changed, 163 insertions(+), 31 deletions(-) create mode 100644 src/it/tests/t0052_bar.rs diff --git a/jay-config/src/theme.rs b/jay-config/src/theme.rs index e9935248..64883b09 100644 --- a/jay-config/src/theme.rs +++ b/jay-config/src/theme.rs @@ -76,7 +76,7 @@ impl Color { Self::BLACK } else if r > a || g > a || b > a { log::warn!( - "f32 values {:?} are not valid valid for a premultiplied color. Using solid black instead.", + "f32 values {:?} are not valid for a premultiplied color. Using solid black instead.", [r, g, b, a] ); Self::BLACK @@ -359,5 +359,9 @@ pub mod sized { /// /// Default: 17 const 03 => BAR_HEIGHT, + /// The width of the bar's separator. + /// + /// Default: 1 + const 04 => BAR_SEPARATOR_WIDTH, } } diff --git a/src/compositor.rs b/src/compositor.rs index 1faae394..5dd23473 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -736,8 +736,8 @@ fn create_dummy_output(state: &Rc) { non_exclusive_rect_rel: Default::default(), bar_rect: Default::default(), bar_rect_rel: Default::default(), - bar_rect_with_underline: Default::default(), - underline_rect_rel: Default::default(), + bar_rect_with_separator: Default::default(), + bar_separator_rect_rel: Default::default(), non_exclusive_rect: Default::default(), render_data: Default::default(), state: state.clone(), diff --git a/src/config/handler.rs b/src/config/handler.rs index 55a3452c..fa2da799 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -2399,6 +2399,7 @@ impl ConfigProxyHandler { TITLE_HEIGHT => ThemeSized::title_height, BORDER_WIDTH => ThemeSized::border_width, BAR_HEIGHT => ThemeSized::bar_height, + BAR_SEPARATOR_WIDTH => ThemeSized::bar_separator_width, _ => return Err(CphError::UnknownSized(sized.0)), }; Ok(sized) diff --git a/src/it/test_config.rs b/src/it/test_config.rs index e942cead..446a675a 100644 --- a/src/it/test_config.rs +++ b/src/it/test_config.rs @@ -16,6 +16,7 @@ use { Axis, Direction, input::{InputDevice, Seat}, keyboard::{Keymap, ModifiedKeySym}, + theme::{BarPosition, sized::BAR_SEPARATOR_WIDTH}, video::{Connector, Transform}, }, std::{cell::Cell, ops::Deref, ptr, rc::Rc, time::Duration}, @@ -301,6 +302,27 @@ impl TestConfig { transform, }) } + + pub fn set_bar_separator_width(&self, width: i32) -> TestResult { + self.send(ClientMessage::SetSize { + sized: BAR_SEPARATOR_WIDTH, + size: width, + }) + } + + pub fn set_bar_position(&self, position: BarPosition) -> TestResult { + self.send(ClientMessage::SetBarPosition { position }) + } + + pub fn set_show_bar(&self, show: bool) -> TestResult { + self.send(ClientMessage::SetShowBar { show }) + } + + pub fn get_show_bar(&self) -> Result { + let reply = self.send_with_reply(ClientMessage::GetShowBar)?; + get_response!(reply, GetShowBar { show }); + Ok(show) + } } impl Drop for TestConfig { diff --git a/src/it/tests.rs b/src/it/tests.rs index 0f571c1a..ce79d2b3 100644 --- a/src/it/tests.rs +++ b/src/it/tests.rs @@ -82,6 +82,7 @@ mod t0048_frame_callback; mod t0049_surface_damage_backend; mod t0050_fifo; mod t0051_pointer_warp; +mod t0052_bar; pub trait TestCase: Sync { fn name(&self) -> &'static str; @@ -152,5 +153,6 @@ pub fn tests() -> Vec<&'static dyn TestCase> { t0049_surface_damage_backend, t0050_fifo, t0051_pointer_warp, + t0052_bar, } } diff --git a/src/it/tests/t0052_bar.rs b/src/it/tests/t0052_bar.rs new file mode 100644 index 00000000..f59c3c3e --- /dev/null +++ b/src/it/tests/t0052_bar.rs @@ -0,0 +1,67 @@ +use { + crate::{ + it::{test_error::TestError, testrun::TestRun}, + tree::OutputNode, + }, + jay_config::theme::BarPosition, + std::rc::Rc, +}; + +testcase!(); + +async fn test(run: Rc) -> Result<(), TestError> { + let setup = run.create_default_setup().await?; + + test_bar(&run, &setup.output, 0).await?; + test_bar(&run, &setup.output, 1).await?; + test_bar(&run, &setup.output, 20).await?; + test_bar(&run, &setup.output, 100).await?; + + Ok(()) +} + +async fn test_bar( + run: &TestRun, + output: &OutputNode, + separator_width: i32, +) -> Result<(), TestError> { + let output_rect = output.global.pos.get(); + + run.cfg.set_bar_separator_width(separator_width)?; + run.cfg.set_bar_position(BarPosition::Top)?; + run.sync().await; + + let bar_height = run.state.theme.sizes.bar_height(); + tassert_eq!(run.state.theme.sizes.bar_separator_width(), separator_width); + + let bar_total_height = bar_height + separator_width; + let bar_rect = output.bar_rect_with_separator.get(); + let ws_rect = output.workspace_rect.get(); + + tassert_eq!(bar_rect.y1(), output_rect.y1()); + tassert_eq!(bar_rect.height(), bar_total_height); + tassert_eq!(ws_rect.y1(), output_rect.y1() + bar_total_height); + tassert_eq!(ws_rect.height(), output_rect.height() - bar_total_height); + + run.cfg.set_bar_position(BarPosition::Bottom)?; + run.sync().await; + + let bar_rect = output.bar_rect_with_separator.get(); + let ws_rect = output.workspace_rect.get(); + tassert_eq!(bar_rect.y2(), output_rect.y2()); + tassert_eq!(bar_rect.height(), bar_total_height); + tassert_eq!(ws_rect.y2(), output_rect.y2() - bar_total_height); + tassert_eq!(ws_rect.height(), output_rect.height() - bar_total_height); + + run.cfg.set_show_bar(false)?; + run.sync().await; + + tassert_eq!(run.cfg.get_show_bar()?, false); + tassert_eq!(output.workspace_rect.get(), output_rect); + tassert_eq!(output.bar_rect_with_separator.get().is_empty(), true); + + run.cfg.set_show_bar(true)?; + run.sync().await; + + Ok(()) +} diff --git a/src/renderer.rs b/src/renderer.rs index 5e3c4020..051987c5 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -110,7 +110,7 @@ impl Renderer<'_> { } let c = theme.colors.separator.get(); self.base - .fill_boxes2(slice::from_ref(&rd.underline), &c, srgb, x, y); + .fill_boxes2(slice::from_ref(&rd.bar_separator), &c, srgb, x, y); let c = theme.colors.unfocused_title_background.get(); self.base .fill_boxes2(&rd.inactive_workspaces, &c, srgb, x, y); diff --git a/src/tasks/connector.rs b/src/tasks/connector.rs index b002919a..54deab43 100644 --- a/src/tasks/connector.rs +++ b/src/tasks/connector.rs @@ -233,8 +233,8 @@ impl ConnectorHandler { non_exclusive_rect_rel: Default::default(), bar_rect: Default::default(), bar_rect_rel: Default::default(), - bar_rect_with_underline: Default::default(), - underline_rect_rel: Default::default(), + bar_rect_with_separator: Default::default(), + bar_separator_rect_rel: Default::default(), render_data: Default::default(), state: self.state.clone(), is_dummy: false, diff --git a/src/theme.rs b/src/theme.rs index 99e18473..6c711c3f 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -450,12 +450,17 @@ impl ThemeSizes { self.title_height.val.get() } } + + pub fn bar_separator_width(&self) -> i32 { + self.bar_separator_width.get() + } } sizes! { title_height = (0, 1000, 17), bar_height = (0, 1000, 17), border_width = (0, 1000, 4), + bar_separator_width = (0, 1000, 1), } pub const DEFAULT_FONT: &str = "monospace 8"; diff --git a/src/tree/output.rs b/src/tree/output.rs index 9a647c6d..9802ada5 100644 --- a/src/tree/output.rs +++ b/src/tree/output.rs @@ -95,8 +95,8 @@ pub struct OutputNode { pub non_exclusive_rect_rel: Cell, pub bar_rect: Cell, pub bar_rect_rel: Cell, - pub bar_rect_with_underline: Cell, - pub underline_rect_rel: Cell, + pub bar_rect_with_separator: Cell, + pub bar_separator_rect_rel: Cell, pub render_data: RefCell, pub state: Rc, pub is_dummy: bool, @@ -593,8 +593,8 @@ impl OutputNode { None }; let active_id = self.workspace.get().map(|w| w.id); - rd.underline = self - .underline_rect_rel + rd.bar_separator = self + .bar_separator_rect_rel .get() .move_(-non_exclusive_rect_rel.x1(), -non_exclusive_rect_rel.y1()); for ws in self.workspaces.iter() { @@ -657,7 +657,7 @@ impl OutputNode { } } let old_full_area = rd.full_area; - rd.full_area = self.bar_rect_with_underline.get(); + rd.full_area = self.bar_rect_with_separator.get(); if self.title_visible.get() { self.state.damage(rd.full_area.union(old_full_area)); } @@ -790,6 +790,7 @@ impl OutputNode { pub fn update_rects(self: &Rc) { let rect = self.global.pos.get(); let bh = self.state.theme.sizes.bar_height(); + let bsw = self.state.theme.sizes.bar_separator_width(); let exclusive = self.exclusive_zones.get(); let y1 = rect.y1() + exclusive.top; let x2 = rect.x2() - exclusive.right; @@ -802,39 +803,44 @@ impl OutputNode { Rect::new_sized_unchecked(exclusive.left, exclusive.top, width, height); let mut bar_rect = Rect::default(); let mut bar_rect_rel = Rect::default(); - let mut bar_rect_with_underline = Rect::default(); - let mut underline_rect_rel = Rect::default(); + let mut bar_rect_with_separator = Rect::default(); + let mut bar_separator_rect_rel = Rect::default(); let mut workspace_rect = non_exclusive_rect; let mut workspace_rect_rel = non_exclusive_rect_rel; if self.state.show_bar.get() { - let underline_rect; + let bar_separator_rect; match self.state.theme.bar_position.get() { BarPosition::Bottom => { workspace_rect = - Rect::new_sized_unchecked(x1, y1, width, (height - bh - 1).max(0)); - bar_rect_with_underline = - Rect::new_sized_unchecked(x1, y1 + height - bh - 1, width, bh + 1); - underline_rect = Rect::new_sized_unchecked(x1, y1 + height - bh - 1, width, 1); + Rect::new_sized_unchecked(x1, y1, width, (height - bh - bsw).max(0)); + bar_rect_with_separator = + Rect::new_sized_unchecked(x1, y1 + height - bh - bsw, width, bh + bsw); + bar_separator_rect = + Rect::new_sized_unchecked(x1, y1 + height - bh - bsw, width, bsw); bar_rect = Rect::new_sized_unchecked(x1, y1 + height - bh, width, bh); } BarPosition::Top | _ => { bar_rect = Rect::new_sized_unchecked(x1, y1, width, bh); - underline_rect = Rect::new_sized_unchecked(x1, y1 + bh, width, 1); - bar_rect_with_underline = Rect::new_sized_unchecked(x1, y1, width, bh + 1); - workspace_rect = - Rect::new_sized_unchecked(x1, y1 + bh + 1, width, (height - bh - 1).max(0)); + bar_separator_rect = Rect::new_sized_unchecked(x1, y1 + bh, width, bsw); + bar_rect_with_separator = Rect::new_sized_unchecked(x1, y1, width, bh + bsw); + workspace_rect = Rect::new_sized_unchecked( + x1, + y1 + bh + bsw, + width, + (height - bh - bsw).max(0), + ); } } bar_rect_rel = bar_rect.move_(-rect.x1(), -rect.y1()); - underline_rect_rel = underline_rect.move_(-rect.x1(), -rect.y1()); + bar_separator_rect_rel = bar_separator_rect.move_(-rect.x1(), -rect.y1()); workspace_rect_rel = workspace_rect.move_(-rect.x1(), -rect.y1()); } self.non_exclusive_rect.set(non_exclusive_rect); self.non_exclusive_rect_rel.set(non_exclusive_rect_rel); self.bar_rect.set(bar_rect); self.bar_rect_rel.set(bar_rect_rel); - self.bar_rect_with_underline.set(bar_rect_with_underline); - self.underline_rect_rel.set(underline_rect_rel); + self.bar_rect_with_separator.set(bar_rect_with_separator); + self.bar_separator_rect_rel.set(bar_separator_rect_rel); self.workspace_rect.set(workspace_rect); self.workspace_rect_rel.set(workspace_rect_rel); self.update_tray_positions(); @@ -1294,8 +1300,8 @@ impl OutputNode { if ws.fullscreen.is_some() { return None; } - let bar_rect_with_underline = self.bar_rect_with_underline.get(); - if bar_rect_with_underline.contains(x_abs, y_abs) { + let bar_rect_with_separator = self.bar_rect_with_separator.get(); + if bar_rect_with_separator.contains(x_abs, y_abs) { let rd = &*self.render_data.borrow(); let bar_rect = self.bar_rect.get(); let (x, _) = bar_rect.translate(x_abs, y_abs); @@ -1350,8 +1356,8 @@ impl OutputNode { if !self.state.show_bar.get() { return None; } - let bar_rect_with_underline = self.bar_rect_with_underline.get(); - if bar_rect_with_underline.not_contains(x_abs, y_abs) { + let bar_rect_with_separator = self.bar_rect_with_separator.get(); + if bar_rect_with_separator.not_contains(x_abs, y_abs) { return None; } let bar_rect = self.bar_rect.get(); @@ -1508,7 +1514,7 @@ pub struct OutputWorkspaceRenderData { pub struct OutputRenderData { pub full_area: Rect, pub active_workspace: Option, - pub underline: Rect, + pub bar_separator: Rect, pub inactive_workspaces: Vec, pub attention_requested_workspaces: Vec, pub captured_inactive_workspaces: Vec, diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index 912bffcb..fa49dd4e 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -209,6 +209,7 @@ pub struct Theme { pub title_font: Option, pub bar_font: Option, pub bar_position: Option, + pub bar_separator_width: Option, } #[derive(Debug, Clone)] diff --git a/toml-config/src/config/parsers/theme.rs b/toml-config/src/config/parsers/theme.rs index d2dea606..efcf6110 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_font, bar_position_val, bar_separator_width), ) = ext.extract(( ( opt(val("attention-requested-bg-color")), @@ -92,6 +92,7 @@ impl Parser for ThemeParser<'_> { ( recover(opt(str("bar-font"))), recover(opt(str("bar-position"))), + recover(opt(s32("bar-separator-width"))), ), ))?; macro_rules! color { @@ -144,6 +145,7 @@ impl Parser for ThemeParser<'_> { title_font: title_font.map(|f| f.value.to_string()), bar_font: bar_font.map(|f| f.value.to_string()), bar_position, + bar_separator_width: bar_separator_width.despan(), }) } } diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 9701c9e2..79949da5 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -976,6 +976,7 @@ impl State { size!(BORDER_WIDTH, border_width); size!(TITLE_HEIGHT, title_height); size!(BAR_HEIGHT, bar_height); + size!(BAR_SEPARATOR_WIDTH, bar_separator_width); macro_rules! font { ($fun:ident, $field:ident) => { if let Some(font) = &theme.$field { diff --git a/toml-spec/spec/spec.generated.json b/toml-spec/spec/spec.generated.json index 140fdd9d..d2f350cf 100644 --- a/toml-spec/spec/spec.generated.json +++ b/toml-spec/spec/spec.generated.json @@ -2012,6 +2012,11 @@ "description": "The position of the bar. Defaults to `top` if not set.", "$ref": "#/$defs/BarPosition" }, + "bar-separator-width": { + "type": "integer", + "description": "The width of the bar's separator. Defaults to 1.", + "minimum": 0.0 + }, "font": { "type": "string", "description": "The name of the font to use." diff --git a/toml-spec/spec/spec.generated.md b/toml-spec/spec/spec.generated.md index dfd477ac..70c30733 100644 --- a/toml-spec/spec/spec.generated.md +++ b/toml-spec/spec/spec.generated.md @@ -4577,6 +4577,16 @@ The table has the following fields: The value of this field should be a [BarPosition](#types-BarPosition). +- `bar-separator-width` (optional): + + The width of the bar's separator. Defaults to 1. + + The value of this field should be a number. + + The numbers should be integers. + + The numbers should be greater than or equal to 0. + - `font` (optional): The name of the font to use. diff --git a/toml-spec/spec/spec.yaml b/toml-spec/spec/spec.yaml index dc89ae52..fa6c122c 100644 --- a/toml-spec/spec/spec.yaml +++ b/toml-spec/spec/spec.yaml @@ -2228,6 +2228,12 @@ Theme: ref: BarPosition required: false description: The position of the bar. Defaults to `top` if not set. + bar-separator-width: + kind: number + integer_only: true + minimum: 0 + required: false + description: The width of the bar's separator. Defaults to 1. font: kind: string required: false