diff --git a/jay-config/src/_private/client.rs b/jay-config/src/_private/client.rs index c4a912ac..b2291c5e 100644 --- a/jay-config/src/_private/client.rs +++ b/jay-config/src/_private/client.rs @@ -1057,6 +1057,16 @@ impl ConfigClient { show } + pub fn set_floating_titles(&self, floating: bool) { + self.send(&ClientMessage::SetFloatingTitles { floating }); + } + + pub fn get_floating_titles(&self) -> bool { + let res = self.send_with_response(&ClientMessage::GetFloatingTitles); + get_response!(res, false, GetFloatingTitles { floating }); + floating + } + pub fn set_bar_position(&self, position: BarPosition) { self.send(&ClientMessage::SetBarPosition { position }); } diff --git a/jay-config/src/_private/ipc.rs b/jay-config/src/_private/ipc.rs index bcd48fcc..be204284 100644 --- a/jay-config/src/_private/ipc.rs +++ b/jay-config/src/_private/ipc.rs @@ -812,6 +812,10 @@ pub enum ClientMessage<'a> { show: bool, }, GetShowTitles, + SetFloatingTitles { + floating: bool, + }, + GetFloatingTitles, GetWorkspaceConnector { workspace: Workspace, }, @@ -1114,6 +1118,9 @@ pub enum Response { GetShowTitles { show: bool, }, + GetFloatingTitles { + floating: bool, + }, GetWorkspaceConnector { connector: Connector, }, diff --git a/jay-config/src/lib.rs b/jay-config/src/lib.rs index 34a58f1e..ce78f0fc 100644 --- a/jay-config/src/lib.rs +++ b/jay-config/src/lib.rs @@ -361,6 +361,27 @@ pub fn toggle_show_titles() { get.set_show_titles(!get.get_show_titles()); } +/// Sets whether title bars float above window content instead of being attached. +/// +/// When enabled, title bars are rendered on top of window content without consuming +/// layout space. Window borders do not extend to include the title bar area. +/// +/// The default is `false`. +pub fn set_floating_titles(floating: bool) { + get!().set_floating_titles(floating) +} + +/// Returns whether title bars float above window content. +pub fn get_floating_titles() -> bool { + get!(false).get_floating_titles() +} + +/// Toggles whether title bars float above window content. +pub fn toggle_floating_titles() { + let get = get!(); + get.set_floating_titles(!get.get_floating_titles()); +} + /// 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 diff --git a/jay-config/src/theme.rs b/jay-config/src/theme.rs index 134498d2..e2218f9e 100644 --- a/jay-config/src/theme.rs +++ b/jay-config/src/theme.rs @@ -370,5 +370,9 @@ pub mod sized { /// /// Default: 0 const 05 => GAP, + /// The gap between the titlebar and the window content in pixels. + /// + /// Default: 0 + const 06 => TITLE_GAP, } } diff --git a/src/config/handler.rs b/src/config/handler.rs index 435e3875..f7b8ef7f 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1421,6 +1421,16 @@ impl ConfigProxyHandler { }); } + fn handle_set_floating_titles(&self, floating: bool) { + self.state.set_floating_titles(floating); + } + + fn handle_get_floating_titles(&self) { + self.respond(Response::GetFloatingTitles { + floating: self.state.theme.floating_titles.get(), + }); + } + fn handle_set_bar_position(&self, position: BarPosition) -> Result<(), CphError> { let Ok(position) = position.try_into() else { return Err(CphError::UnknownBarPosition(position)); @@ -2441,6 +2451,7 @@ impl ConfigProxyHandler { BAR_HEIGHT => ThemeSized::bar_height, BAR_SEPARATOR_WIDTH => ThemeSized::bar_separator_width, GAP => ThemeSized::gap, + TITLE_GAP => ThemeSized::title_gap, _ => return Err(CphError::UnknownSized(sized.0)), }; Ok(sized) @@ -3291,6 +3302,10 @@ impl ConfigProxyHandler { ClientMessage::GetShowBar => self.handle_get_show_bar(), ClientMessage::SetShowTitles { show } => self.handle_set_show_titles(show), ClientMessage::GetShowTitles => self.handle_get_show_titles(), + ClientMessage::SetFloatingTitles { floating } => { + self.handle_set_floating_titles(floating) + } + ClientMessage::GetFloatingTitles => self.handle_get_floating_titles(), ClientMessage::SetBarPosition { position } => self .handle_set_bar_position(position) .wrn("set_bar_position")?, diff --git a/src/renderer.rs b/src/renderer.rs index 3ef2e3da..2b025344 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -277,65 +277,70 @@ impl Renderer<'_> { self.render_tl_aux(placeholder.tl_data(), bounds, true); } - pub fn render_container(&mut self, container: &ContainerNode, x: i32, y: i32) { - { - let srgb_srgb = self.state.color_manager.srgb_gamma22(); - let srgb = &srgb_srgb.linear; - let perceptual = RenderIntent::Perceptual; - let rd = container.render_data.borrow_mut(); - let c = self.state.theme.colors.unfocused_title_background.get(); + fn render_container_decorations(&mut self, container: &ContainerNode, x: i32, y: i32) { + let srgb_srgb = self.state.color_manager.srgb_gamma22(); + let srgb = &srgb_srgb.linear; + let perceptual = RenderIntent::Perceptual; + let rd = container.render_data.borrow_mut(); + let c = self.state.theme.colors.unfocused_title_background.get(); + self.base + .fill_boxes2(&rd.title_rects, &c, srgb, perceptual, x, y); + let c = self.state.theme.colors.focused_title_background.get(); + self.base + .fill_boxes2(&rd.active_title_rects, &c, srgb, perceptual, x, y); + let c = self.state.theme.colors.attention_requested_background.get(); + self.base + .fill_boxes2(&rd.attention_title_rects, &c, srgb, perceptual, x, y); + let c = self.state.theme.colors.separator.get(); + self.base + .fill_boxes2(&rd.underline_rects, &c, srgb, perceptual, x, y); + let c = self.state.theme.colors.border.get(); + self.base + .fill_boxes2(&rd.border_rects, &c, srgb, perceptual, x, y); + if let Some(lar) = &rd.last_active_rect { + let c = self + .state + .theme + .colors + .focused_inactive_title_background + .get(); self.base - .fill_boxes2(&rd.title_rects, &c, srgb, perceptual, x, y); - let c = self.state.theme.colors.focused_title_background.get(); - self.base - .fill_boxes2(&rd.active_title_rects, &c, srgb, perceptual, x, y); - let c = self.state.theme.colors.attention_requested_background.get(); - self.base - .fill_boxes2(&rd.attention_title_rects, &c, srgb, perceptual, x, y); - let c = self.state.theme.colors.separator.get(); - self.base - .fill_boxes2(&rd.underline_rects, &c, srgb, perceptual, x, y); - let c = self.state.theme.colors.border.get(); - self.base - .fill_boxes2(&rd.border_rects, &c, srgb, perceptual, x, y); - if let Some(lar) = &rd.last_active_rect { - let c = self - .state - .theme - .colors - .focused_inactive_title_background - .get(); - self.base - .fill_boxes2(std::slice::from_ref(lar), &c, srgb, perceptual, x, y); - } - if let Some(titles) = rd.titles.get(&self.base.scale) { - for title in titles { - let rect = title.rect.move_(x, y); - let bounds = self.base.scale_rect(rect); - let (x, y) = self.base.scale_point(rect.x1(), rect.y1()); - self.base.render_texture( - &title.tex, - None, - x, - y, - None, - None, - self.base.scale, - Some(&bounds), - None, - AcquireSync::None, - ReleaseSync::None, - false, - srgb_srgb, - perceptual, - AlphaMode::PremultipliedElectrical, - ); - } + .fill_boxes2(std::slice::from_ref(lar), &c, srgb, perceptual, x, y); + } + if let Some(titles) = rd.titles.get(&self.base.scale) { + for title in titles { + let rect = title.rect.move_(x, y); + let bounds = self.base.scale_rect(rect); + let (x, y) = self.base.scale_point(rect.x1(), rect.y1()); + self.base.render_texture( + &title.tex, + None, + x, + y, + None, + None, + self.base.scale, + Some(&bounds), + None, + AcquireSync::None, + ReleaseSync::None, + false, + srgb_srgb, + perceptual, + AlphaMode::PremultipliedElectrical, + ); } } + } + + pub fn render_container(&mut self, container: &ContainerNode, x: i32, y: i32) { + let floating = self.state.theme.floating_titles.get(); + + self.render_container_decorations(container, x, y); + if let Some(child) = container.mono_child.get() { let mb = container.mono_body.get(); - if self.state.theme.sizes.gap.get() > 0 && !child.node.node_is_container() { + if self.state.theme.sizes.gap.get() != 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(); @@ -346,22 +351,48 @@ impl Renderer<'_> { } 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 srgb = &srgb_srgb.linear; + let perceptual = RenderIntent::Perceptual; + if floating { + if !child.node.node_is_container() { + let body_frame = [ + Rect::new_sized_saturating(mb.x1() - bw, mb.y1(), bw, mb.y2() - mb.y1()), + Rect::new_sized_saturating(mb.x2(), mb.y1(), bw, mb.y2() - mb.y1()), + Rect::new_sized_saturating(mb.x1() - bw, mb.y1() - bw, full_w + 2 * bw, bw), + Rect::new_sized_saturating(mb.x1() - bw, mb.y2(), full_w + 2 * bw, bw), + ]; + self.base.fill_boxes2(&body_frame, c, srgb, perceptual, x, y); + } + let th = self.state.theme.title_height(); + if th > 0 { + for tab in container.children.iter() { + let tr = tab.title_rect.get(); + let tc = if tab.active.get() { + &focused_border_color + } else { + &border_color + }; + let tw = tr.width(); + let tab_frame = [ + Rect::new_sized_saturating(tr.x1() - bw, tr.y1() - bw, tw + 2 * bw, bw), + Rect::new_sized_saturating(tr.x1() - bw, tr.y1(), bw, th), + Rect::new_sized_saturating(tr.x2(), tr.y1(), bw, th), + Rect::new_sized_saturating(tr.x1() - bw, tr.y1() + th, tw + 2 * bw, bw), + ]; + self.base.fill_boxes2(&tab_frame, tc, srgb, perceptual, x, y); + } + } + } else if !child.node.node_is_container() { + let full_h = mb.y2(); + 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, perceptual, x, y); + } } let body = mb.move_(x, y); let body = self.base.scale_rect(body); @@ -371,7 +402,7 @@ impl Renderer<'_> { .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, 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(); @@ -393,41 +424,46 @@ impl Renderer<'_> { 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 srgb = &srgb_srgb.linear; + let c = if floating { + if child.active.get() { &focused_border_color } else { &border_color } + } else if child.border_color_is_focused.get() { + &focused_border_color + } else { + &border_color + }; + let title_rect = child.title_rect.get(); + let full_w = body.width(); + let perceptual = RenderIntent::Perceptual; + if floating && tpuh > 0 { + let tw = title_rect.width(); + let title_h = title_rect.height(); + let title_frame = [ + Rect::new_sized_saturating(title_rect.x1() - bw, title_rect.y1() - bw, tw + 2 * bw, bw), + Rect::new_sized_saturating(title_rect.x1() - bw, title_rect.y1(), bw, title_h), + Rect::new_sized_saturating(title_rect.x2(), title_rect.y1(), bw, title_h), + Rect::new_sized_saturating(title_rect.x1() - bw, title_rect.y1() + title_h, tw + 2 * bw, bw), + ]; + self.base.fill_boxes2(&title_frame, c, srgb, perceptual, x, y); + if !child.node.node_is_container() && gap != 0 { + let body_frame = [ + Rect::new_sized_saturating(body.x1() - bw, body.y1(), bw, body.y2() - body.y1()), + Rect::new_sized_saturating(body.x2(), body.y1(), bw, body.y2() - body.y1()), + Rect::new_sized_saturating(body.x1() - bw, body.y1() - bw, full_w + 2 * bw, bw), + Rect::new_sized_saturating(body.x1() - bw, body.y2(), full_w + 2 * bw, bw), + ]; + self.base.fill_boxes2(&body_frame, c, srgb, perceptual, x, y); + } + } else if !child.node.node_is_container() && gap != 0 { 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, - ), + 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, - ); + self.base.fill_boxes2(&frame_rects, c, srgb, perceptual, x, y); } } let body = body.move_(x, y); @@ -438,6 +474,7 @@ impl Renderer<'_> { .node_render(self, x + content.x1(), y + content.y1(), Some(&body)); } } + self.render_tl_aux(container.tl_data(), None, false); } diff --git a/src/state.rs b/src/state.rs index afd0e545..ea68b130 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1861,6 +1861,11 @@ impl State { self.spaces_changed(); } + pub fn set_floating_titles(&self, floating: bool) { + self.theme.floating_titles.set(floating); + self.spaces_changed(); + } + pub fn set_ui_drag_enabled(&self, enabled: bool) { self.ui_drag_enabled.set(enabled); } diff --git a/src/theme.rs b/src/theme.rs index 2ee80489..2a1e1703 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -587,6 +587,7 @@ sizes! { border_width = (0, 1000, 4), bar_separator_width = (0, 1000, 1), gap = (0, 1000, 0), + title_gap = (0, 1000, 5), } impl StaticText for ThemeSized { @@ -597,6 +598,7 @@ impl StaticText for ThemeSized { ThemeSized::border_width => "Border Width", ThemeSized::bar_separator_width => "Bar Separator Width", ThemeSized::gap => "Gap", + ThemeSized::title_gap => "Title Gap", } } } @@ -649,6 +651,7 @@ pub struct Theme { pub title_font: CloneCell>>, pub default_font: Arc, pub show_titles: Cell, + pub floating_titles: Cell, pub bar_position: Cell, } @@ -663,6 +666,7 @@ impl Default for Theme { title_font: Default::default(), default_font, show_titles: Cell::new(true), + floating_titles: Cell::new(false), bar_position: Default::default(), } } @@ -689,9 +693,22 @@ impl Theme { if self.show_titles.get() { 1 } else { 0 } } + pub fn floating_title_top_margin(&self) -> i32 { + if self.floating_titles.get() && self.sizes.gap.get() != 0 { + self.sizes.border_width.get() + } else { + 0 + } + } + pub fn title_plus_underline_height(&self) -> i32 { if self.show_titles.get() { - self.sizes.title_height.get() + 1 + if self.floating_titles.get() && self.sizes.gap.get() != 0 { + let bw = self.sizes.border_width.get(); + 3 * bw + self.sizes.title_height.get() + self.sizes.title_gap.get() + } else { + self.sizes.title_height.get() + 1 + } } else { 0 } diff --git a/src/tree/container.rs b/src/tree/container.rs index df1c87b7..395f6d8c 100644 --- a/src/tree/container.rs +++ b/src/tree/container.rs @@ -383,16 +383,26 @@ impl ContainerNode { let mb = self.mono_body.get(); return (mb.width(), mb.height()); } - let spacing = self.state.theme.sizes.gap.get().max( - self.state.theme.sizes.border_width.get(), - ); + let gap = self.state.theme.sizes.gap.get(); + let bw = self.state.theme.sizes.border_width.get(); + let floating = self.state.theme.floating_titles.get() && gap != 0; let nc = self.num_children.get() as i32 + 1; match self.split.get() { ContainerSplit::Horizontal => { + let spacing = if floating { + self.state.theme.sizes.title_gap.get() + 2 * bw + } else { + gap.max(bw) + }; let content_w = self.width.get().sub((nc - 1) * spacing).max(0); (content_w / nc, self.height.get().sub(tpuh).max(0)) } ContainerSplit::Vertical => { + let spacing = if floating { + self.state.theme.sizes.title_gap.get() + bw + } else { + gap.max(bw) + }; let content_h = self .height .get() @@ -407,6 +417,7 @@ impl ContainerNode { self.update_content_size(); // log::info!("on_spaces_changed"); self.schedule_layout(); + self.schedule_compute_render_positions(); } pub fn on_colors_changed(self: &Rc) { @@ -416,7 +427,7 @@ impl ContainerNode { } fn damage(&self) { - let bw = if self.state.theme.sizes.gap.get() > 0 { + let bw = if self.state.theme.sizes.gap.get() != 0 { self.state.theme.sizes.border_width.get() } else { 0 @@ -485,8 +496,15 @@ impl ContainerNode { let th = self.state.theme.title_height(); let bw = self.state.theme.sizes.border_width.get(); + let bw_top = self.state.theme.floating_title_top_margin(); + let floating = self.state.theme.floating_titles.get(); + let spacing = if floating { + self.state.theme.sizes.title_gap.get() + 2 * bw + } else { + bw + }; let num_children = self.num_children.get() as i32; - let content_width = self.width.get().sub(bw * (num_children - 1)).max(0); + let content_width = self.width.get().sub(spacing * (num_children - 1)).max(0); let width_per_child = content_width / num_children; let mut rem = content_width % num_children; let mut pos = 0; @@ -498,18 +516,28 @@ impl ContainerNode { } child .title_rect - .set(Rect::new_sized_saturating(pos, 0, width, th)); - pos += width + bw; + .set(Rect::new_sized_saturating(pos, bw_top, width, th)); + pos += width + spacing; } } fn perform_split_layout(self: &Rc) { let sum_factors = self.sum_factors.get(); + let gap = self.state.theme.sizes.gap.get(); let border_width = self.state.theme.sizes.border_width.get(); - let spacing = self.state.theme.sizes.gap.get().max(border_width); + let floating = self.state.theme.floating_titles.get() && gap != 0; let title_height_tmp = self.state.theme.title_height(); - let title_plus_underline_height = self.state.theme.title_plus_underline_height(); + let tpuh = self.state.theme.title_plus_underline_height(); let split = self.split.get(); + let spacing = if floating { + let title_gap = self.state.theme.sizes.title_gap.get(); + match split { + ContainerSplit::Horizontal => title_gap + 2 * border_width, + ContainerSplit::Vertical => title_gap + border_width, + } + } else { + gap.max(border_width) + }; let (content_size, other_content_size) = match split { ContainerSplit::Horizontal => (self.content_width.get(), self.content_height.get()), ContainerSplit::Vertical => (self.content_height.get(), self.content_width.get()), @@ -527,24 +555,14 @@ impl ContainerNode { body_size = body_size.min(remaining_content_size); remaining_content_size -= body_size; let (x1, y1, width, height) = match split { - ContainerSplit::Horizontal => ( - pos, - title_plus_underline_height, - body_size, - other_content_size, - ), - _ => ( - 0, - pos + title_plus_underline_height, - other_content_size, - body_size, - ), + ContainerSplit::Horizontal => (pos, tpuh, body_size, other_content_size), + _ => (0, pos + tpuh, other_content_size, body_size), }; let body = Rect::new_sized_saturating(x1, y1, width, height); child.body.set(body); pos += body_size + spacing; if split == ContainerSplit::Vertical { - pos += title_plus_underline_height; + pos += tpuh; } } if remaining_content_size > 0 { @@ -561,39 +579,28 @@ impl ContainerNode { let (x1, y1, width, height, size) = match split { ContainerSplit::Horizontal => { let width = body.width() + add; - ( - pos, - title_plus_underline_height, - width, - other_content_size, - width, - ) + (pos, tpuh, width, other_content_size, width) } _ => { let height = body.height() + add; - ( - 0, - pos + title_plus_underline_height, - other_content_size, - height, - height, - ) + (0, pos + tpuh, other_content_size, height, height) } }; body = Rect::new_sized_saturating(x1, y1, width, height); child.body.set(body); pos += size + spacing; if split == ContainerSplit::Vertical { - pos += title_plus_underline_height; + pos += tpuh; } } } + let bw_top = self.state.theme.floating_title_top_margin(); self.sum_factors.set(1.0); for child in self.children.iter() { let body = child.body.get(); child.title_rect.set(Rect::new_sized_saturating( body.x1(), - body.y1() - title_plus_underline_height, + body.y1() - tpuh + bw_top, body.width(), title_height_tmp, )); @@ -604,25 +611,33 @@ impl ContainerNode { } fn update_content_size(&self) { + let gap = self.state.theme.sizes.gap.get(); 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 floating = self.state.theme.floating_titles.get() && gap != 0; + let title_gap = self.state.theme.sizes.title_gap.get(); + let tpuh = self.state.theme.title_plus_underline_height(); let nc = self.num_children.get(); match self.split.get() { ContainerSplit::Horizontal => { + let spacing = if floating { + title_gap + 2 * border_width + } else { + gap.max(border_width) + }; 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)); + self.content_height.set(self.height.get().sub(tpuh).max(0)); } ContainerSplit::Vertical => { + let spacing = if floating { + title_gap + border_width + } else { + gap.max(border_width) + }; let new_content_size = self .height .get() - .sub( - title_plus_underline_height - + (nc - 1) as i32 * (spacing + title_plus_underline_height), - ) + .sub(tpuh + (nc - 1) as i32 * (spacing + tpuh)) .max(0); self.content_height.set(new_content_size); self.content_width.set(self.width.get()); @@ -630,9 +645,9 @@ impl ContainerNode { } self.mono_body.set(Rect::new_sized_saturating( 0, - title_plus_underline_height, + tpuh, self.width.get(), - self.height.get() - title_plus_underline_height, + self.height.get() - tpuh, )); } @@ -904,9 +919,9 @@ impl ContainerNode { rect.height() + tuh, )); } - if gap > 0 && !mono && !child.node.node_is_container() { + if gap != 0 && !mono && !child.node.node_is_container() { child.border_color_is_focused.set(child.active.get()); - } else if gap == 0 && i > 0 { + } else if i > 0 { let sep = if mono { Rect::new_sized_saturating(rect.x1() - bw, 0, bw, th) } else if split == ContainerSplit::Horizontal { @@ -914,7 +929,10 @@ impl ContainerNode { } else { Rect::new_sized_saturating(0, rect.y1() - bw, cwidth, bw) }; - rd.border_rects.push(sep); + let floating = self.state.theme.floating_titles.get(); + if gap == 0 || (mono && !floating) { + rd.border_rects.push(sep); + } } if child.active.get() { rd.active_title_rects.push(rect); @@ -925,7 +943,7 @@ impl ContainerNode { } else { rd.title_rects.push(rect); } - if !mono { + if !mono && (!self.state.theme.floating_titles.get() || gap == 0) { let rect = Rect::new_sized_saturating(rect.x1(), rect.y2(), rect.width(), 1); rd.underline_rects.push(rect); } @@ -937,10 +955,14 @@ impl ContainerNode { } } } - if mono { + if mono && (!self.state.theme.floating_titles.get() || gap == 0) { rd.underline_rects .push(Rect::new_sized_saturating(0, th, cwidth, tuh)); } + if gap == 0 && th > 0 { + rd.underline_rects + .push(Rect::new_sized_saturating(0, 0, cwidth, tuh)); + } if self.toplevel_data.visible.get() && (mono || split == ContainerSplit::Horizontal) { self.state .damage(Rect::new_sized_saturating(abs_x, abs_y, cwidth, tpuh)); @@ -1248,7 +1270,7 @@ 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() { + if self.state.theme.sizes.gap.get() != 0 && self.toplevel_data.visible.get() { self.damage(); } if let Some(parent) = self.toplevel_data.parent.get() { diff --git a/toml-config/src/config.rs b/toml-config/src/config.rs index e6edf19a..ed067c08 100644 --- a/toml-config/src/config.rs +++ b/toml-config/src/config.rs @@ -80,6 +80,8 @@ pub enum SimpleCommand { ToggleBar, ShowTitles(bool), ToggleTitles, + FloatTitles(bool), + ToggleFloatTitles, FocusHistory(Timeline), FocusLayerRel(LayerDirection), FocusTiles, @@ -224,6 +226,8 @@ pub struct Theme { pub bar_position: Option, pub bar_separator_width: Option, pub gap: Option, + pub floating_titles: Option, + pub title_gap: Option, } #[derive(Debug, Clone)] diff --git a/toml-config/src/config/parsers/action.rs b/toml-config/src/config/parsers/action.rs index 9cfa631f..9f8aef29 100644 --- a/toml-config/src/config/parsers/action.rs +++ b/toml-config/src/config/parsers/action.rs @@ -153,6 +153,9 @@ impl ActionParser<'_> { "show-titles" => ShowTitles(true), "hide-titles" => ShowTitles(false), "toggle-titles" => ToggleTitles, + "float-titles" => FloatTitles(true), + "unfloat-titles" => FloatTitles(false), + "toggle-float-titles" => ToggleFloatTitles, "focus-prev" => FocusHistory(Timeline::Older), "focus-next" => FocusHistory(Timeline::Newer), "focus-below" => FocusLayerRel(LayerDirection::Below), diff --git a/toml-config/src/config/parsers/theme.rs b/toml-config/src/config/parsers/theme.rs index dae2674d..aa593d71 100644 --- a/toml-config/src/config/parsers/theme.rs +++ b/toml-config/src/config/parsers/theme.rs @@ -3,7 +3,7 @@ use { config::{ Theme, context::Context, - extractor::{Extractor, ExtractorError, opt, recover, s32, str, val}, + extractor::{Extractor, ExtractorError, bol, opt, recover, s32, str, val}, parser::{DataType, ParseResult, Parser, UnexpectedDataType}, parsers::color::ColorParser, }, @@ -63,7 +63,7 @@ impl Parser for ThemeParser<'_> { font, title_font, ), - (bar_font, bar_position_val, bar_separator_width, gap), + (bar_font, bar_position_val, bar_separator_width, gap, floating_titles), ) = ext.extract(( ( opt(val("attention-requested-bg-color")), @@ -94,8 +94,10 @@ impl Parser for ThemeParser<'_> { recover(opt(str("bar-position"))), recover(opt(s32("bar-separator-width"))), recover(opt(s32("gap"))), + recover(opt(bol("floating-titles"))), ), ))?; + let (title_gap,) = ext.extract((recover(opt(s32("title-gap"))),))?; macro_rules! color { ($e:expr) => { match $e { @@ -148,6 +150,8 @@ impl Parser for ThemeParser<'_> { bar_position, bar_separator_width: bar_separator_width.despan(), gap: gap.despan(), + floating_titles: floating_titles.despan(), + title_gap: title_gap.despan(), }) } } diff --git a/toml-config/src/lib.rs b/toml-config/src/lib.rs index 6a70dba4..dfaabab5 100644 --- a/toml-config/src/lib.rs +++ b/toml-config/src/lib.rs @@ -39,7 +39,8 @@ use { on_devices_enumerated, on_idle, on_unload, quit, reload, set_color_management_enabled, set_default_workspace_capture, set_explicit_sync_enabled, set_float_above_fullscreen, set_idle, set_idle_grace_period, - set_middle_click_paste_enabled, set_show_bar, set_show_float_pin_icon, set_show_titles, + set_floating_titles, set_middle_click_paste_enabled, set_show_bar, set_show_float_pin_icon, + set_show_titles, set_ui_drag_enabled, set_ui_drag_threshold, status::{set_i3bar_separator, set_status, set_status_command, unset_status_command}, switch_to_vt, @@ -48,7 +49,7 @@ use { reset_colors, reset_font, reset_sizes, set_bar_font, set_bar_position, set_font, set_title_font, }, - toggle_float_above_fullscreen, toggle_show_bar, toggle_show_titles, + toggle_float_above_fullscreen, toggle_floating_titles, toggle_show_bar, toggle_show_titles, video::{ ColorSpace, Connector, DrmDevice, Eotf, connectors, create_virtual_output, drm_devices, on_connector_connected, on_connector_disconnected, on_graphics_initialized, @@ -206,6 +207,10 @@ impl Action { SimpleCommand::ToggleBar => b.new(toggle_show_bar), SimpleCommand::ShowTitles(show) => b.new(move || set_show_titles(show)), SimpleCommand::ToggleTitles => b.new(toggle_show_titles), + SimpleCommand::FloatTitles(floating) => { + b.new(move || set_floating_titles(floating)) + } + SimpleCommand::ToggleFloatTitles => b.new(toggle_floating_titles), SimpleCommand::FocusHistory(timeline) => { let persistent = state.persistent.clone(); b.new(move || persistent.seat.focus_history(timeline)) @@ -1004,6 +1009,7 @@ impl State { size!(BAR_HEIGHT, bar_height); size!(BAR_SEPARATOR_WIDTH, bar_separator_width); size!(GAP, gap); + size!(TITLE_GAP, title_gap); macro_rules! font { ($fun:ident, $field:ident) => { if let Some(font) = &theme.$field { @@ -1637,6 +1643,9 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc