1
0
Fork 0
forked from wry/wry

all: add support for hy3 like tiling

This commit is contained in:
kossLAN 2026-04-10 13:16:35 -04:00
parent a41dbae899
commit cea4187fc0
No known key found for this signature in database
21 changed files with 1237 additions and 48 deletions

View file

@ -2035,6 +2035,42 @@ impl ConfigClient {
radius radius
} }
pub fn seat_toggle_expand(&self, seat: Seat) {
self.send(&ClientMessage::SeatToggleExpand { seat });
}
pub fn seat_toggle_tab(&self, seat: Seat) {
self.send(&ClientMessage::SeatToggleTab { seat });
}
pub fn seat_make_group(&self, seat: Seat, axis: Axis, ephemeral: bool) {
self.send(&ClientMessage::SeatMakeGroup {
seat,
axis,
ephemeral,
});
}
pub fn seat_change_group_opposite(&self, seat: Seat) {
self.send(&ClientMessage::SeatChangeGroupOpposite { seat });
}
pub fn seat_equalize(&self, seat: Seat, recursive: bool) {
self.send(&ClientMessage::SeatEqualize { seat, recursive });
}
pub fn set_autotile(&self, enabled: bool) {
self.send(&ClientMessage::SetAutotile { enabled });
}
pub fn set_tab_title_align(&self, align: u32) {
self.send(&ClientMessage::SetTabTitleAlign { align });
}
pub fn seat_move_tab(&self, seat: Seat, right: bool) {
self.send(&ClientMessage::SeatMoveTab { seat, right });
}
fn handle_msg(&self, msg: &[u8]) { fn handle_msg(&self, msg: &[u8]) {
self.handle_msg2(msg); self.handle_msg2(msg);
self.dispatch_futures(); self.dispatch_futures();

View file

@ -884,6 +884,34 @@ pub enum ClientMessage<'a> {
radius: f32, radius: f32,
}, },
GetCornerRadius, GetCornerRadius,
SeatToggleExpand {
seat: Seat,
},
SeatToggleTab {
seat: Seat,
},
SeatMakeGroup {
seat: Seat,
axis: Axis,
ephemeral: bool,
},
SeatChangeGroupOpposite {
seat: Seat,
},
SeatEqualize {
seat: Seat,
recursive: bool,
},
SetAutotile {
enabled: bool,
},
SetTabTitleAlign {
align: u32,
},
SeatMoveTab {
seat: Seat,
right: bool,
},
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]

View file

@ -677,6 +677,33 @@ impl Seat {
pub fn unstable_set_mouse_follows_focus(self, enabled: bool) { pub fn unstable_set_mouse_follows_focus(self, enabled: bool) {
get!().seat_set_mouse_follows_focus(self, enabled) get!().seat_set_mouse_follows_focus(self, enabled)
} }
/// Toggles tabbed mode on the focused window's parent container.
pub fn toggle_tab(self) {
get!().seat_toggle_tab(self)
}
/// Wraps the focused child in a new sub-container with the given split axis.
pub fn make_group(self, axis: Axis, ephemeral: bool) {
get!().seat_make_group(self, axis, ephemeral)
}
/// Toggles the parent container's split direction (H↔V).
pub fn change_group_opposite(self) {
get!().seat_change_group_opposite(self)
}
/// Resets all siblings' size factors to equal.
pub fn equalize(self, recursive: bool) {
get!().seat_equalize(self, recursive)
}
/// Move the active tab left or right within the current tab bar.
///
/// Equivalent to hy3's `movewindow` within a tabbed group.
pub fn move_tab(self, right: bool) {
get!().seat_move_tab(self, right)
}
} }
/// A focus-follows-mouse mode. /// A focus-follows-mouse mode.

View file

@ -394,6 +394,30 @@ pub fn get_corner_radius() -> f32 {
get!(0.0).get_corner_radius() get!(0.0).get_corner_radius()
} }
/// Enables or disables autotiling.
///
/// When enabled, new windows are automatically placed in a perpendicular
/// sub-container if the predicted body would be narrower than tall (or vice versa).
///
/// The default is `false`.
pub fn set_autotile(enabled: bool) {
get!().set_autotile(enabled)
}
/// Sets the horizontal alignment of title text within tab buttons.
///
/// - `"start"` — left-aligned (default)
/// - `"center"` — centered
/// - `"end"` — right-aligned
pub fn set_tab_title_align(align: &str) {
let val = match align {
"center" => 1,
"end" => 2,
_ => 0, // start
};
get!().set_tab_title_align(val)
}
/// Sets a callback to run when this config is unloaded. /// Sets a callback to run when this config is unloaded.
/// ///
/// Only one callback can be set at a time. If another callback is already set, it will be /// Only one callback can be set at a time. If another callback is already set, it will be

View file

@ -302,6 +302,38 @@ pub mod colors {
/// ///
/// Default: `#9d28c67f`. /// Default: `#9d28c67f`.
const 15 => HIGHLIGHT_COLOR, const 15 => HIGHLIGHT_COLOR,
/// The background color of an active (focused) tab.
///
/// Default: `#33ccff40`.
const 16 => TAB_ACTIVE_BACKGROUND_COLOR,
/// The border color of an active (focused) tab.
///
/// Default: `#33ccffee`.
const 17 => TAB_ACTIVE_BORDER_COLOR,
/// The background color of an inactive tab.
///
/// Default: `#222222`.
const 18 => TAB_INACTIVE_BACKGROUND_COLOR,
/// The border color of an inactive tab.
///
/// Default: `#333333`.
const 19 => TAB_INACTIVE_BORDER_COLOR,
/// The text color of an active (focused) tab.
///
/// Default: `#ffffff`.
const 20 => TAB_ACTIVE_TEXT_COLOR,
/// The text color of an inactive tab.
///
/// Default: `#888888`.
const 21 => TAB_INACTIVE_TEXT_COLOR,
/// The background color of the tab bar strip.
///
/// Default: `#111111`.
const 22 => TAB_BAR_BACKGROUND_COLOR,
/// The background color of a tab that has requested attention.
///
/// Default: `#23092c`.
const 23 => TAB_ATTENTION_BACKGROUND_COLOR,
} }
/// Sets the color of GUI element. /// Sets the color of GUI element.
@ -374,5 +406,29 @@ pub mod sized {
/// ///
/// Default: 0 /// Default: 0
const 06 => TITLE_GAP, const 06 => TITLE_GAP,
/// The height of the tab bar in pixels.
///
/// Default: 22
const 07 => TAB_BAR_HEIGHT,
/// The padding between tabs in the tab bar in pixels.
///
/// Default: 6
const 08 => TAB_BAR_PADDING,
/// The corner radius of tabs in the tab bar in pixels.
///
/// Default: 6
const 09 => TAB_BAR_RADIUS,
/// The border width of tabs in the tab bar in pixels.
///
/// Default: 2
const 10 => TAB_BAR_BORDER_WIDTH,
/// The horizontal padding within each tab for text in pixels.
///
/// Default: 4
const 11 => TAB_BAR_TEXT_PADDING,
/// The gap between the tab bar and the window content below in pixels.
///
/// Default: 4
const 12 => TAB_BAR_GAP,
} }
} }

View file

@ -62,6 +62,7 @@ use {
WorkspaceDisplayOrder, WorkspaceNode, container_layout, container_render_positions, WorkspaceDisplayOrder, WorkspaceNode, container_layout, container_render_positions,
float_layout, output_render_data, float_layout, output_render_data,
placeholder_render_textures, placeholder_render_textures,
container_tab_render_textures,
}, },
user_session::import_environment, user_session::import_environment,
utils::{ utils::{
@ -265,6 +266,7 @@ fn start_compositor2(
pending_toplevel_screencasts: Default::default(), pending_toplevel_screencasts: Default::default(),
pending_screencast_reallocs_or_reconfigures: Default::default(), pending_screencast_reallocs_or_reconfigures: Default::default(),
pending_placeholder_render_textures: Default::default(), pending_placeholder_render_textures: Default::default(),
pending_container_tab_render_textures: Default::default(),
dbus: Dbus::new(&engine, &ring, &run_toplevel), dbus: Dbus::new(&engine, &ring, &run_toplevel),
fdcloser: FdCloser::new(), fdcloser: FdCloser::new(),
logger: logger.clone(), logger: logger.clone(),
@ -514,6 +516,11 @@ fn start_global_event_handlers(state: &Rc<State>) -> Vec<SpawnedFuture<()>> {
Phase::PostLayout, Phase::PostLayout,
placeholder_render_textures(state.clone()), placeholder_render_textures(state.clone()),
), ),
eng.spawn2(
"container tab textures",
Phase::PostLayout,
container_tab_render_textures(state.clone()),
),
eng.spawn2( eng.spawn2(
"output render", "output render",
Phase::PostLayout, Phase::PostLayout,

View file

@ -1757,6 +1757,41 @@ impl ConfigProxyHandler {
Ok(()) Ok(())
} }
fn handle_seat_toggle_tab(&self, seat: Seat) -> Result<(), CphError> {
let seat = self.get_seat(seat)?;
seat.toggle_tab();
Ok(())
}
fn handle_seat_make_group(
&self,
seat: Seat,
axis: Axis,
ephemeral: bool,
) -> Result<(), CphError> {
let seat = self.get_seat(seat)?;
seat.make_group(axis.into(), ephemeral);
Ok(())
}
fn handle_seat_change_group_opposite(&self, seat: Seat) -> Result<(), CphError> {
let seat = self.get_seat(seat)?;
seat.change_group_opposite();
Ok(())
}
fn handle_seat_equalize(&self, seat: Seat, recursive: bool) -> Result<(), CphError> {
let seat = self.get_seat(seat)?;
seat.equalize(recursive);
Ok(())
}
fn handle_seat_move_tab(&self, seat: Seat, right: bool) -> Result<(), CphError> {
let seat = self.get_seat(seat)?;
seat.move_tab(right);
Ok(())
}
fn handle_get_window_split(&self, window: Window) -> Result<(), CphError> { fn handle_get_window_split(&self, window: Window) -> Result<(), CphError> {
let window = self.get_window(window)?; let window = self.get_window(window)?;
self.respond(Response::GetWindowSplit { self.respond(Response::GetWindowSplit {
@ -2464,6 +2499,12 @@ impl ConfigProxyHandler {
BAR_SEPARATOR_WIDTH => ThemeSized::bar_separator_width, BAR_SEPARATOR_WIDTH => ThemeSized::bar_separator_width,
GAP => ThemeSized::gap, GAP => ThemeSized::gap,
TITLE_GAP => ThemeSized::title_gap, TITLE_GAP => ThemeSized::title_gap,
TAB_BAR_HEIGHT => ThemeSized::tab_bar_height,
TAB_BAR_PADDING => ThemeSized::tab_bar_padding,
TAB_BAR_RADIUS => ThemeSized::tab_bar_radius,
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,
_ => return Err(CphError::UnknownSized(sized.0)), _ => return Err(CphError::UnknownSized(sized.0)),
}; };
Ok(sized) Ok(sized)
@ -2541,6 +2582,14 @@ impl ConfigProxyHandler {
BAR_STATUS_TEXT_COLOR => ThemeColor::bar_text, BAR_STATUS_TEXT_COLOR => ThemeColor::bar_text,
ATTENTION_REQUESTED_BACKGROUND_COLOR => ThemeColor::attention_requested_background, ATTENTION_REQUESTED_BACKGROUND_COLOR => ThemeColor::attention_requested_background,
HIGHLIGHT_COLOR => ThemeColor::highlight, HIGHLIGHT_COLOR => ThemeColor::highlight,
TAB_ACTIVE_BACKGROUND_COLOR => ThemeColor::tab_active_background,
TAB_ACTIVE_BORDER_COLOR => ThemeColor::tab_active_border,
TAB_INACTIVE_BACKGROUND_COLOR => ThemeColor::tab_inactive_background,
TAB_INACTIVE_BORDER_COLOR => ThemeColor::tab_inactive_border,
TAB_ACTIVE_TEXT_COLOR => ThemeColor::tab_active_text,
TAB_INACTIVE_TEXT_COLOR => ThemeColor::tab_inactive_text,
TAB_BAR_BACKGROUND_COLOR => ThemeColor::tab_bar_background,
TAB_ATTENTION_BACKGROUND_COLOR => ThemeColor::tab_attention_background,
_ => return Err(CphError::UnknownColor(colorable.0)), _ => return Err(CphError::UnknownColor(colorable.0)),
}; };
Ok(colorable) Ok(colorable)
@ -3448,6 +3497,40 @@ impl ConfigProxyHandler {
} => self } => self
.handle_window_resize(window, dx1, dy1, dx2, dy2) .handle_window_resize(window, dx1, dy1, dx2, dy2)
.wrn("window_resize")?, .wrn("window_resize")?,
ClientMessage::SeatToggleTab { seat } => self
.handle_seat_toggle_tab(seat)
.wrn("seat_toggle_tab")?,
ClientMessage::SeatMakeGroup {
seat,
axis,
ephemeral,
} => self
.handle_seat_make_group(seat, axis, ephemeral)
.wrn("seat_make_group")?,
ClientMessage::SeatChangeGroupOpposite { seat } => self
.handle_seat_change_group_opposite(seat)
.wrn("seat_change_group_opposite")?,
ClientMessage::SeatEqualize { seat, recursive } => self
.handle_seat_equalize(seat, recursive)
.wrn("seat_equalize")?,
ClientMessage::SetAutotile { enabled } => {
self.state.theme.autotile_enabled.set(enabled);
}
ClientMessage::SeatToggleExpand { .. } => {
// Removed feature; kept for binary protocol compatibility.
}
ClientMessage::SetTabTitleAlign { align } => {
use crate::theme::TabTitleAlign;
let val = match align {
1 => TabTitleAlign::Center,
2 => TabTitleAlign::End,
_ => TabTitleAlign::Start,
};
self.state.theme.tab_title_align.set(val);
}
ClientMessage::SeatMoveTab { seat, right } => self
.handle_seat_move_tab(seat, right)
.wrn("seat_move_tab")?,
} }
Ok(()) Ok(())
} }

View file

@ -306,6 +306,8 @@ pub struct RoundedFillRect {
pub border_width: f32, pub border_width: f32,
/// Output scale for antialiasing. /// Output scale for antialiasing.
pub scale: f32, pub scale: f32,
/// Sort order hint within the RoundedFill bucket (lower renders first).
pub z_order: u32,
} }
impl RoundedFillRect { impl RoundedFillRect {

View file

@ -254,6 +254,7 @@ struct VulkanRoundedFillOp {
border_width: f32, border_width: f32,
scale: f32, scale: f32,
range_address: DeviceAddress, range_address: DeviceAddress,
z_order: u32,
} }
struct VulkanRoundedTexOp { struct VulkanRoundedTexOp {
@ -923,7 +924,7 @@ impl VulkanRenderer {
enum Key { enum Key {
Fill { color: [u32; 4] }, Fill { color: [u32; 4] },
Tex(usize), Tex(usize),
RoundedFill { color: [u32; 4] }, RoundedFill { z_order: u32, color: [u32; 4] },
RoundedTex(usize), RoundedTex(usize),
} }
match o { match o {
@ -932,6 +933,7 @@ impl VulkanRenderer {
}, },
VulkanOp::Tex(t) => Key::Tex(t.index), VulkanOp::Tex(t) => Key::Tex(t.index),
VulkanOp::RoundedFill(f) => Key::RoundedFill { VulkanOp::RoundedFill(f) => Key::RoundedFill {
z_order: f.z_order,
color: f.color.map(|c| c.to_bits()), color: f.color.map(|c| c.to_bits()),
}, },
VulkanOp::RoundedTex(t) => Key::RoundedTex(t.index), VulkanOp::RoundedTex(t) => Key::RoundedTex(t.index),
@ -1152,6 +1154,7 @@ impl VulkanRenderer {
border_width: rf.border_width, border_width: rf.border_width,
scale: rf.scale, scale: rf.scale,
range_address: 0, range_address: 0,
z_order: rf.z_order,
})); }));
} }
} }
@ -2385,12 +2388,13 @@ impl VulkanRenderer {
}; };
(opaque, c.target) (opaque, c.target)
} }
GfxApiOpt::RoundedFillRect(_) => { GfxApiOpt::RoundedFillRect(rf) => {
// Rounded rects are never fully opaque due to AA at corners // Rounded rects are never fully opaque due to AA at corners,
continue; // but they do paint pixels and need paint regions.
(false, rf.rect)
} }
GfxApiOpt::RoundedCopyTexture(_) => { GfxApiOpt::RoundedCopyTexture(ct) => {
continue; (false, ct.target)
} }
}; };
if opaque || bb.is_none() { if opaque || bb.is_none() {

View file

@ -83,7 +83,7 @@ use {
rect::Rect, rect::Rect,
state::{DeviceHandlerData, State}, state::{DeviceHandlerData, State},
tree::{ tree::{
ContainerNode, ContainerSplit, Direction, FoundNode, Node, NodeId, NodeLayer, ContainerNode, ContainerSplit, ChangeGroupAction, Direction, FoundNode, Node, NodeId, NodeLayer,
NodeLayerLink, NodeLocation, OutputNode, StackedNode, ToplevelNode, WorkspaceNode, NodeLayerLink, NodeLocation, OutputNode, StackedNode, ToplevelNode, WorkspaceNode,
generic_node_visitor, toplevel_create_split, toplevel_parent_container, generic_node_visitor, toplevel_create_split, toplevel_parent_container,
toplevel_set_floating, toplevel_set_workspace, toplevel_set_floating, toplevel_set_workspace,
@ -745,6 +745,40 @@ impl WlSeatGlobal {
toplevel_create_split(&self.state, tl, axis); toplevel_create_split(&self.state, tl, axis);
} }
pub fn toggle_tab(&self) {
if let Some(c) = self.kb_parent_container() {
c.change_group(ChangeGroupAction::ToggleTab);
}
}
pub fn make_group(&self, axis: ContainerSplit, ephemeral: bool) {
if let Some(c) = self.kb_parent_container() {
c.make_group(axis, ephemeral);
}
}
pub fn change_group_opposite(&self) {
if let Some(c) = self.kb_parent_container() {
c.change_group(ChangeGroupAction::Opposite);
}
}
pub fn equalize(&self, recursive: bool) {
if let Some(c) = self.kb_parent_container() {
if recursive {
c.equalize_recursive();
} else {
c.equalize();
}
}
}
pub fn move_tab(&self, right: bool) {
if let Some(c) = self.kb_parent_container() {
c.move_tab(right);
}
}
pub fn focus_parent(self: &Rc<Self>) { pub fn focus_parent(self: &Rc<Self>) {
if let Some(tl) = self.keyboard_node.get().node_toplevel() if let Some(tl) = self.keyboard_node.get().node_toplevel()
&& let Some(parent) = tl.tl_data().parent.get() && let Some(parent) = tl.tl_data().parent.get()

View file

@ -15,7 +15,7 @@ use {
theme::{Color, CornerRadius}, theme::{Color, CornerRadius},
tree::{ tree::{
ContainerNode, DisplayNode, FloatNode, OutputNode, PlaceholderNode, ToplevelData, ContainerNode, DisplayNode, FloatNode, OutputNode, PlaceholderNode, ToplevelData,
ToplevelNodeBase, WorkspaceNode, ToplevelNodeBase, WorkspaceNode, tab_bar::TabBar,
}, },
}, },
std::{ops::Deref, rc::Rc, slice}, std::{ops::Deref, rc::Rc, slice},
@ -277,6 +277,105 @@ impl Renderer<'_> {
self.render_tl_aux(placeholder.tl_data(), bounds, true); self.render_tl_aux(placeholder.tl_data(), bounds, true);
} }
fn render_tab_bar(&mut self, tab_bar: &TabBar, x: i32, y: i32, _container_width: i32) {
let srgb_srgb = self.state.color_manager.srgb_gamma22();
let srgb = &srgb_srgb.linear;
let perceptual = RenderIntent::Perceptual;
let scalef = self.base.scalef as f32;
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 bar_height = tab_bar.height;
let render_scale = tab_bar.render_scale;
// Vulkan sorts ops: Fill < Tex < RoundedFill (by z_order, color) < RoundedTex.
// 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);
// 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);
// 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,
);
// Rounded border ring on top (z_order=1, renders after bg).
if border_width > 0 {
self.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,
);
}
// 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),
);
}
}
}
fn render_container_decorations(&mut self, container: &ContainerNode, x: i32, y: i32) { fn render_container_decorations(&mut self, container: &ContainerNode, x: i32, y: i32) {
let srgb_srgb = self.state.color_manager.srgb_gamma22(); let srgb_srgb = self.state.color_manager.srgb_gamma22();
let srgb = &srgb_srgb.linear; let srgb = &srgb_srgb.linear;
@ -291,6 +390,13 @@ impl Renderer<'_> {
self.render_container_decorations(container, x, y); self.render_container_decorations(container, x, y);
if let Some(child) = container.mono_child.get() { if let Some(child) = container.mono_child.get() {
// Render tab bar if present.
{
let tab_bar = container.tab_bar.borrow();
if let Some(tb) = tab_bar.as_ref() {
self.render_tab_bar(tb, x, y, container.width.get());
}
}
let mb = container.mono_body.get(); let mb = container.mono_body.get();
if self.state.theme.sizes.gap.get() != 0 { if self.state.theme.sizes.gap.get() != 0 {
let srgb_srgb = self.state.color_manager.srgb_gamma22(); let srgb_srgb = self.state.color_manager.srgb_gamma22();
@ -308,21 +414,22 @@ impl Renderer<'_> {
let perceptual = RenderIntent::Perceptual; let perceptual = RenderIntent::Perceptual;
if !child.node.node_is_container() { if !child.node.node_is_container() {
let cr = self.state.theme.corner_radius.get(); let cr = self.state.theme.corner_radius.get();
let full_h = mb.y2(); let frame_y = mb.y1();
let frame_h = mb.height();
if cr.is_zero() { if cr.is_zero() {
let frame_rects = [ let frame_rects = [
Rect::new_sized_saturating(mb.x1() - bw, 0, bw, full_h), Rect::new_sized_saturating(mb.x1() - bw, frame_y, bw, frame_h),
Rect::new_sized_saturating(mb.x2(), 0, bw, full_h), Rect::new_sized_saturating(mb.x2(), frame_y, bw, frame_h),
Rect::new_sized_saturating(mb.x1() - bw, -bw, full_w + 2 * bw, bw), Rect::new_sized_saturating(mb.x1() - bw, frame_y - bw, full_w + 2 * bw, bw),
Rect::new_sized_saturating(mb.x1() - bw, full_h, full_w + 2 * bw, bw), Rect::new_sized_saturating(mb.x1() - bw, frame_y + frame_h, full_w + 2 * bw, bw),
]; ];
self.base.fill_boxes2(&frame_rects, c, srgb, perceptual, x, y); self.base.fill_boxes2(&frame_rects, c, srgb, perceptual, x, y);
} else { } else {
let outer = Rect::new_sized_saturating( let outer = Rect::new_sized_saturating(
mb.x1() - bw, mb.x1() - bw,
-bw, frame_y - bw,
full_w + 2 * bw, full_w + 2 * bw,
full_h + 2 * bw, frame_h + 2 * bw,
); );
let scalef = self.base.scalef as f32; let scalef = self.base.scalef as f32;
let scaled_cr = cr.scaled_by(scalef); let scaled_cr = cr.scaled_by(scalef);

View file

@ -259,6 +259,20 @@ impl RendererBase<'_> {
render_intent: RenderIntent, render_intent: RenderIntent,
corner_radius: CornerRadius, corner_radius: CornerRadius,
border_width: f32, border_width: f32,
) {
self.fill_rounded_rect_z(rect, color, alpha, cd, render_intent, corner_radius, border_width, 0)
}
pub fn fill_rounded_rect_z(
&mut self,
rect: Rect,
color: &Color,
alpha: Option<f32>,
cd: &Rc<LinearColorDescription>,
render_intent: RenderIntent,
corner_radius: CornerRadius,
border_width: f32,
z_order: u32,
) { ) {
if *color == Color::TRANSPARENT { if *color == Color::TRANSPARENT {
return; return;
@ -288,6 +302,7 @@ impl RendererBase<'_> {
corner_radius: cr, corner_radius: cr,
border_width, border_width,
scale, scale,
z_order,
})); }));
} }

View file

@ -195,6 +195,7 @@ pub struct State {
pub pending_toplevel_screencasts: AsyncQueue<Rc<JayScreencast>>, pub pending_toplevel_screencasts: AsyncQueue<Rc<JayScreencast>>,
pub pending_screencast_reallocs_or_reconfigures: AsyncQueue<Rc<JayScreencast>>, pub pending_screencast_reallocs_or_reconfigures: AsyncQueue<Rc<JayScreencast>>,
pub pending_placeholder_render_textures: AsyncQueue<Rc<PlaceholderNode>>, pub pending_placeholder_render_textures: AsyncQueue<Rc<PlaceholderNode>>,
pub pending_container_tab_render_textures: AsyncQueue<Rc<ContainerNode>>,
pub dbus: Dbus, pub dbus: Dbus,
pub fdcloser: Arc<FdCloser>, pub fdcloser: Arc<FdCloser>,
pub logger: Option<Arc<Logger>>, pub logger: Option<Arc<Logger>>,
@ -1112,6 +1113,7 @@ impl State {
self.pending_toplevel_screencasts.clear(); self.pending_toplevel_screencasts.clear();
self.pending_screencast_reallocs_or_reconfigures.clear(); self.pending_screencast_reallocs_or_reconfigures.clear();
self.pending_placeholder_render_textures.clear(); self.pending_placeholder_render_textures.clear();
self.pending_container_tab_render_textures.clear();
self.render_ctx_watchers.clear(); self.render_ctx_watchers.clear();
self.workspace_watchers.clear(); self.workspace_watchers.clear();
self.toplevel_lists.clear(); self.toplevel_lists.clear();

View file

@ -454,6 +454,14 @@ colors! {
bar_text = (0xff, 0xff, 0xff), bar_text = (0xff, 0xff, 0xff),
attention_requested_background = (0x23, 0x09, 0x2c), attention_requested_background = (0x23, 0x09, 0x2c),
highlight = (0x9d, 0x28, 0xc6, 0x7f), highlight = (0x9d, 0x28, 0xc6, 0x7f),
tab_active_background = (0x4c, 0x78, 0x99),
tab_active_border = (0x28, 0x55, 0x77),
tab_inactive_background = (0x22, 0x22, 0x22),
tab_inactive_border = (0x33, 0x33, 0x33),
tab_active_text = (0xff, 0xff, 0xff),
tab_inactive_text = (0x88, 0x88, 0x88),
tab_bar_background = (0x00, 0x00, 0x00, 0x00),
tab_attention_background = (0x23, 0x09, 0x2c),
} }
impl StaticText for ThemeColor { impl StaticText for ThemeColor {
@ -476,6 +484,14 @@ impl StaticText for ThemeColor {
ThemeColor::bar_text => "Bar Text", ThemeColor::bar_text => "Bar Text",
ThemeColor::attention_requested_background => "Attention Requested", ThemeColor::attention_requested_background => "Attention Requested",
ThemeColor::highlight => "Highlight", ThemeColor::highlight => "Highlight",
ThemeColor::tab_active_background => "Tab Background (active)",
ThemeColor::tab_active_border => "Tab Border (active)",
ThemeColor::tab_inactive_background => "Tab Background (inactive)",
ThemeColor::tab_inactive_border => "Tab Border (inactive)",
ThemeColor::tab_active_text => "Tab Text (active)",
ThemeColor::tab_inactive_text => "Tab Text (inactive)",
ThemeColor::tab_bar_background => "Tab Bar Background",
ThemeColor::tab_attention_background => "Tab Attention Background",
} }
} }
} }
@ -588,6 +604,12 @@ sizes! {
bar_separator_width = (0, 1000, 1), bar_separator_width = (0, 1000, 1),
gap = (0, 1000, 0), gap = (0, 1000, 0),
title_gap = (0, 1000, 5), title_gap = (0, 1000, 5),
tab_bar_height = (0, 1000, 22),
tab_bar_padding = (0, 1000, 6),
tab_bar_radius = (0, 1000, 6),
tab_bar_border_width = (0, 1000, 2),
tab_bar_text_padding = (0, 1000, 4),
tab_bar_gap = (0, 1000, 4),
} }
impl StaticText for ThemeSized { impl StaticText for ThemeSized {
@ -599,6 +621,12 @@ impl StaticText for ThemeSized {
ThemeSized::bar_separator_width => "Bar Separator Width", ThemeSized::bar_separator_width => "Bar Separator Width",
ThemeSized::gap => "Gap", ThemeSized::gap => "Gap",
ThemeSized::title_gap => "Title Gap", ThemeSized::title_gap => "Title Gap",
ThemeSized::tab_bar_height => "Tab Bar Height",
ThemeSized::tab_bar_padding => "Tab Bar Padding",
ThemeSized::tab_bar_radius => "Tab Bar Radius",
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",
} }
} }
} }
@ -732,6 +760,15 @@ impl CornerRadius {
} }
} }
/// Horizontal alignment of title text inside tab buttons.
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum TabTitleAlign {
#[default]
Start,
Center,
End,
}
pub struct Theme { pub struct Theme {
pub colors: ThemeColors, pub colors: ThemeColors,
pub sizes: ThemeSizes, pub sizes: ThemeSizes,
@ -745,6 +782,8 @@ pub struct Theme {
pub floating_titles: Cell<bool>, pub floating_titles: Cell<bool>,
pub bar_position: Cell<BarPosition>, pub bar_position: Cell<BarPosition>,
pub corner_radius: Cell<CornerRadius>, pub corner_radius: Cell<CornerRadius>,
pub autotile_enabled: Cell<bool>,
pub tab_title_align: Cell<TabTitleAlign>,
} }
impl Default for Theme { impl Default for Theme {
@ -761,6 +800,8 @@ impl Default for Theme {
floating_titles: Cell::new(false), floating_titles: Cell::new(false),
bar_position: Default::default(), bar_position: Default::default(),
corner_radius: Cell::new(CornerRadius::default()), corner_radius: Cell::new(CornerRadius::default()),
autotile_enabled: Cell::new(false),
tab_title_align: Cell::new(TabTitleAlign::default()),
} }
} }
} }
@ -770,6 +811,10 @@ impl Theme {
self.bar_font.get().unwrap_or_else(|| self.font.get()) self.bar_font.get().unwrap_or_else(|| self.font.get())
} }
pub fn title_font(&self) -> Arc<String> {
self.title_font.get().unwrap_or_else(|| self.font.get())
}
pub fn title_height(&self) -> i32 { pub fn title_height(&self) -> i32 {
0 0
} }

View file

@ -50,6 +50,7 @@ mod float;
mod output; mod output;
mod placeholder; mod placeholder;
mod stacked; mod stacked;
pub mod tab_bar;
mod toplevel; mod toplevel;
mod walker; mod walker;
mod workspace; mod workspace;

View file

@ -12,20 +12,24 @@ use {
}, },
rect::Rect, rect::Rect,
renderer::Renderer, renderer::Renderer,
scale::Scale,
state::State, state::State,
text::TextTexture,
tree::{ tree::{
ContainingNode, Direction, FindTreeResult, FindTreeUsecase, FloatNode, FoundNode, Node, ContainingNode, Direction, FindTreeResult, FindTreeUsecase, FloatNode, FoundNode, Node,
NodeId, NodeLayerLink, NodeLocation, OutputNode, TddType, TileDragDestination, NodeId, NodeLayerLink, NodeLocation, OutputNode, TddType, TileDragDestination,
ToplevelData, ToplevelNode, ToplevelNodeBase, ToplevelType, WorkspaceNode, ToplevelData, ToplevelNode, ToplevelNodeBase, ToplevelType, WorkspaceNode,
default_tile_drag_bounds, toplevel_set_workspace, default_tile_drag_bounds, tab_bar::{TabBar, TabBarEntry}, toplevel_set_workspace,
walker::NodeVisitor, walker::NodeVisitor,
}, },
utils::{ utils::{
clonecell::CloneCell, clonecell::CloneCell,
errorfmt::ErrorFmt,
event_listener::LazyEventSource, event_listener::LazyEventSource,
hash_map_ext::HashMapExt, hash_map_ext::HashMapExt,
linkedlist::{LinkedList, LinkedNode, NodeRef}, linkedlist::{LinkedList, LinkedNode, NodeRef},
numcell::NumCell, numcell::NumCell,
on_drop_event::OnDropEvent,
rc_eq::rc_eq, rc_eq::rc_eq,
threshold_counter::ThresholdCounter, threshold_counter::ThresholdCounter,
}, },
@ -85,6 +89,28 @@ pub enum ContainerFocus {
tree_id!(ContainerNodeId); tree_id!(ContainerNodeId);
/// Ephemeral group state (hy3-style auto-collapse).
///
/// An ephemeral container is a transient grouping that auto-collapses back to
/// its single child when all but one child is removed.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
pub enum Ephemeral {
/// Normal container — never auto-collapses.
#[default]
Off,
/// Ephemeral container — when child count drops to 1, collapse.
On,
}
/// Actions for the `changegroup` operation.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ChangeGroupAction {
/// Toggle between Horizontal and Vertical.
Opposite,
/// Toggle between tabbed (mono) and split mode.
ToggleTab,
}
#[derive(Default)] #[derive(Default)]
pub struct ContainerRenderData { pub struct ContainerRenderData {
pub border_rects: Vec<Rect>, pub border_rects: Vec<Rect>,
@ -120,6 +146,9 @@ pub struct ContainerNode {
pub child_added: Rc<LazyEventSource>, pub child_added: Rc<LazyEventSource>,
pub child_removed: Rc<LazyEventSource>, pub child_removed: Rc<LazyEventSource>,
pub all_children_resized: Rc<LazyEventSource>, pub all_children_resized: Rc<LazyEventSource>,
pub tab_bar: RefCell<Option<TabBar>>,
pub update_tab_textures_scheduled: Cell<bool>,
pub ephemeral: Cell<Ephemeral>,
} }
impl Debug for ContainerNode { impl Debug for ContainerNode {
@ -231,6 +260,9 @@ impl ContainerNode {
child_added: state.lazy_event_sources.create_source(), child_added: state.lazy_event_sources.create_source(),
child_removed: state.lazy_event_sources.create_source(), child_removed: state.lazy_event_sources.create_source(),
all_children_resized: state.post_layout_event_sources.create_source(), all_children_resized: state.post_layout_event_sources.create_source(),
tab_bar: RefCell::new(None),
update_tab_textures_scheduled: Cell::new(false),
ephemeral: Cell::new(Ephemeral::Off),
}); });
child.tl_set_parent(slf.clone()); child.tl_set_parent(slf.clone());
slf.pull_child_properties(&child_node_ref); slf.pull_child_properties(&child_node_ref);
@ -335,6 +367,8 @@ impl ContainerNode {
self.sum_factors.set(sum_factors); self.sum_factors.set(sum_factors);
if self.mono_child.is_some() { if self.mono_child.is_some() {
self.activate_child(&new_ref); self.activate_child(&new_ref);
self.rebuild_tab_bar();
self.damage();
} }
// log::info!("add_child"); // log::info!("add_child");
self.schedule_layout(); self.schedule_layout();
@ -549,11 +583,19 @@ impl ContainerNode {
self.content_width.set(self.width.get()); self.content_width.set(self.width.get());
} }
} }
let tab_bar_height = if self.mono_child.is_some() {
// Tab bar sits above the window with a configurable gap.
let tbh = self.state.theme.sizes.tab_bar_height.get();
let gap = self.state.theme.sizes.tab_bar_gap.get();
tbh + gap
} else {
0
};
self.mono_body.set(Rect::new_sized_saturating( self.mono_body.set(Rect::new_sized_saturating(
0, 0,
0, tab_bar_height,
self.width.get(), self.width.get(),
self.height.get(), (self.height.get() - tab_bar_height).max(0),
)); ));
} }
@ -588,7 +630,9 @@ impl ContainerNode {
dist_left, dist_left,
dist_right, dist_right,
} => { } => {
let prev = op.child.prev().unwrap(); let Some(prev) = op.child.prev() else {
return;
};
let prev_body = prev.body.get(); let prev_body = prev.body.get();
let child_body = op.child.body.get(); let child_body = op.child.body.get();
let (prev_factor, child_factor) = match self.split.get() { let (prev_factor, child_factor) = match self.split.get() {
@ -656,6 +700,14 @@ impl ContainerNode {
} }
} }
fn schedule_update_tab_textures(self: &Rc<Self>) {
if !self.update_tab_textures_scheduled.replace(true) {
self.state
.pending_container_tab_render_textures
.push(self.clone());
}
}
fn compute_render_positions(&self) { fn compute_render_positions(&self) {
self.compute_render_positions_scheduled.set(false); self.compute_render_positions_scheduled.set(false);
let mut rd = self.render_data.borrow_mut(); let mut rd = self.render_data.borrow_mut();
@ -713,6 +765,13 @@ impl ContainerNode {
} }
} }
self.mono_child.set(Some(child.clone())); self.mono_child.set(Some(child.clone()));
// Keep focus_history in sync with mono_child so that
// toggle_tab and other operations that use focus_history.last()
// return the correct child.
child
.focus_history
.set(Some(self.focus_history.add_last(child.clone())));
self.rebuild_tab_bar();
if self.toplevel_data.visible.get() { if self.toplevel_data.visible.get() {
self.perform_layout(); self.perform_layout();
child.node.tl_set_visible(true); child.node.tl_set_visible(true);
@ -764,9 +823,25 @@ impl ContainerNode {
} }
} }
} }
self.mono_child.set(child); self.mono_child.set(child.clone());
// log::info!("set_mono"); if child.is_some() {
self.rebuild_tab_bar();
} else {
*self.tab_bar.borrow_mut() = None;
}
self.update_content_size();
self.damage();
self.schedule_layout(); self.schedule_layout();
// Notify parent to rebuild its tab bar if we're a child of a mono container.
// Our tab title prefix changes when we enter/leave mono mode ([T] vs [H]/[V]).
if let Some(parent) = self.toplevel_data.parent.get() {
if let Some(pc) = parent.node_into_container() {
if pc.mono_child.is_some() {
pc.rebuild_tab_bar();
pc.damage();
}
}
}
} }
pub fn set_split(self: &Rc<Self>, split: ContainerSplit) { pub fn set_split(self: &Rc<Self>, split: ContainerSplit) {
@ -777,6 +852,233 @@ impl ContainerNode {
} }
} }
/// Rebuild the tab bar entries from the current children.
///
/// Called when entering mono mode or when children change while in mono mode.
fn rebuild_tab_bar(self: &Rc<Self>) {
self.rebuild_tab_bar_with_override(None, None);
}
/// Generate a tab title for a child node. For windows this is the window
/// title or app_id. For container (group) children, we show a layout
/// prefix like hy3: `[H] child_title`, `[V] child_title`, `[T] child_title`.
fn get_child_tab_title(
&self,
child: &ContainerChild,
override_id: Option<NodeId>,
override_title: Option<&str>,
) -> String {
let child_id = child.node.node_id();
// If this child is a container (group), show layout prefix + focused child title.
if let Some(cn) = child.node.clone().node_into_container() {
let prefix = if cn.mono_child.is_some() {
"[T]"
} else {
match cn.split.get() {
ContainerSplit::Horizontal => "[H]",
ContainerSplit::Vertical => "[V]",
}
};
// Get the focused child's title recursively.
let inner = if let Some(focused) = cn.focus_history.last() {
let inner_child = ContainerChild {
node: focused.node.clone(),
active: Cell::new(false),
body: Default::default(),
content: Default::default(),
factor: Cell::new(0.0),
focus_history: Default::default(),
attention_requested: Cell::new(false),
border_color_is_focused: Default::default(),
};
cn.get_child_tab_title(&inner_child, override_id, override_title)
} else {
"Group".to_string()
};
return format!("{} {}", prefix, inner);
}
// Window node: use override, title, app_id, or "untitled".
let raw_title = if override_id == Some(child_id) && override_title.is_some() {
override_title.unwrap().to_string()
} else {
child.node.tl_data().title.borrow().clone()
};
if !raw_title.is_empty() {
return raw_title;
}
if override_id == Some(child_id) {
let app = child.node.tl_data().app_id.borrow().clone();
if !app.is_empty() {
return app;
}
} else {
let app = child.node.tl_data().app_id.borrow().clone();
if !app.is_empty() {
return app;
}
}
"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).
fn rebuild_tab_bar_with_override(
self: &Rc<Self>,
override_id: Option<NodeId>,
override_title: Option<&str>,
) {
let mono = self.mono_child.get();
if mono.is_none() {
*self.tab_bar.borrow_mut() = None;
return;
}
let mono_ref = mono.as_ref().unwrap();
let active_id = mono_ref.node.node_id();
let height = self.state.theme.sizes.tab_bar_height.get();
let render_scale = self.workspace.get().output.get().global.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);
self.schedule_update_tab_textures();
}
/// Wrap the focused child in a new sub-container with the given split direction.
///
/// This is hy3's `makegroup` operation.
pub fn make_group(self: &Rc<Self>, split: ContainerSplit, ephemeral: bool) {
let Some(focused) = self.focus_history.last() else {
return;
};
if self.num_children.get() <= 1 {
return;
}
let focused_node = focused.node.clone();
// Record the sibling that comes AFTER the focused child so we can
// insert the new group at the same position.
let next_sibling: Option<Rc<dyn ToplevelNode>> = {
let nodes = self.child_nodes.borrow();
nodes
.get(&focused_node.node_id())
.and_then(|ln| ln.next())
.map(|n| n.node.clone())
};
// Temporarily disable ephemeral collapse during make_group so
// removing the focused child doesn't destroy this container.
let was_ephemeral = self.ephemeral.replace(Ephemeral::Off);
self.clone().cnode_remove_child2(&*focused_node, true);
self.ephemeral.set(was_ephemeral);
let sub = ContainerNode::new(
&self.state,
&self.workspace.get(),
focused_node,
split,
);
if ephemeral {
sub.ephemeral.set(Ephemeral::On);
}
// Insert at the original position instead of appending to the end.
if let Some(ref next) = next_sibling {
self.add_child_before(&**next, sub);
} else {
// Was the last child — append.
self.append_child(sub);
}
}
/// Change this container's split direction (hy3's `changegroup`).
///
/// `opposite` toggles H↔V, `toggletab` toggles between mono and split.
pub fn change_group(self: &Rc<Self>, action: ChangeGroupAction) {
match action {
ChangeGroupAction::Opposite => {
let new_split = match self.split.get() {
ContainerSplit::Horizontal => ContainerSplit::Vertical,
ContainerSplit::Vertical => ContainerSplit::Horizontal,
};
self.set_split(new_split);
}
ChangeGroupAction::ToggleTab => {
if self.mono_child.is_some() {
// Exit mono/tabbed mode.
self.set_mono(None);
} else if let Some(focused) = self.focus_history.last() {
// Enter mono/tabbed mode with the focused child.
self.set_mono(Some(&*focused.node));
}
}
}
}
/// Reset all children's factors to equal (hy3's `equalize`).
pub fn equalize(self: &Rc<Self>) {
let n = self.num_children.get();
if n == 0 {
return;
}
let factor = 1.0 / n as f64;
let mut sum = 0.0;
for child in self.children.iter() {
child.factor.set(factor);
sum += factor;
}
self.sum_factors.set(sum);
self.schedule_layout();
}
/// Recursively equalize all descendant containers.
pub fn equalize_recursive(self: &Rc<Self>) {
self.equalize();
for child in self.children.iter() {
if let Some(cn) = child.node.clone().node_into_container() {
cn.equalize_recursive();
}
}
}
/// Move the currently active tab left or right within the tab bar.
///
/// This is the equivalent of hy3's `movewindow` within a tabbed group.
pub fn move_tab(self: &Rc<Self>, right: bool) {
let mc = match self.mono_child.get() {
Some(mc) => mc,
None => return,
};
let child_nodes = self.child_nodes.borrow();
let Some(link) = child_nodes.get(&mc.node.node_id()) else {
return;
};
let active_ref = link.to_ref();
if right {
if let Some(next) = active_ref.next() {
// Move active tab after the next sibling (moves right).
next.append_existing(&active_ref);
}
} else {
if let Some(prev) = active_ref.prev() {
// Move active tab before the previous sibling (moves left).
prev.prepend_existing(&active_ref);
}
}
drop(child_nodes);
self.rebuild_tab_bar();
self.damage();
}
fn parent_container(&self) -> Option<Rc<ContainerNode>> { fn parent_container(&self) -> Option<Rc<ContainerNode>> {
self.toplevel_data self.toplevel_data
.parent .parent
@ -955,6 +1257,42 @@ impl ContainerNode {
} }
pub fn insert_child(self: &Rc<Self>, node: Rc<dyn ToplevelNode>, direction: Direction) { pub fn insert_child(self: &Rc<Self>, node: Rc<dyn ToplevelNode>, direction: Direction) {
// Autotile: if the container would become too narrow/tall, wrap the
// focused child and new node in a perpendicular sub-container.
if self.state.theme.autotile_enabled.get() && self.mono_child.is_none() {
let (pw, ph) = self.predict_child_body_size();
let opposite = match self.split.get() {
ContainerSplit::Horizontal if pw > 0 && ph > 0 && pw < ph => {
Some(ContainerSplit::Vertical)
}
ContainerSplit::Vertical if pw > 0 && ph > 0 && ph < pw => {
Some(ContainerSplit::Horizontal)
}
_ => None,
};
if let Some(opp_split) = opposite {
if let Some(focused) = self.focus_history.last() {
if self.num_children.get() <= 1 {
// Single child, autotile not applicable.
} else {
let focused_node = focused.node.clone();
let was_ephemeral = self.ephemeral.replace(Ephemeral::Off);
self.clone().cnode_remove_child2(&*focused_node, true);
self.ephemeral.set(was_ephemeral);
let sub = ContainerNode::new(
&self.state,
&self.workspace.get(),
focused_node,
opp_split,
);
sub.ephemeral.set(Ephemeral::On);
sub.append_child(node);
self.append_child(sub);
return;
}
}
}
}
let (split, right) = direction_to_split(direction); let (split, right) = direction_to_split(direction);
if split != self.split.get() || right { if split != self.split.get() || right {
self.append_child(node); self.append_child(node);
@ -1050,16 +1388,39 @@ impl ContainerNode {
if !pressed { if !pressed {
return; return;
} }
// Handle tab bar clicks in mono mode.
if self.mono_child.is_some() {
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;
drop(tab_bar);
drop(seat_datas);
let children = self.child_nodes.borrow();
if let Some(child) = children.get(&child_id) {
let child_ref = child.to_ref();
drop(children);
self.activate_child(&child_ref);
}
return;
}
}
}
}
let (kind, child) = 'res: { let (kind, child) = 'res: {
let mono = self.mono_child.is_some(); let mono = self.mono_child.is_some();
for child in self.children.iter() { for child in self.children.iter() {
if !mono { if !mono {
if self.split.get() == ContainerSplit::Horizontal { if self.split.get() == ContainerSplit::Horizontal {
if seat_data.x < child.body.get().x1() { if seat_data.x < child.body.get().x1() {
let Some(prev) = child.prev() else {
continue;
};
break 'res ( break 'res (
SeatOpKind::Resize { SeatOpKind::Resize {
dist_left: seat_data.x dist_left: seat_data.x
- child.prev().unwrap().body.get().x2(), - prev.body.get().x2(),
dist_right: child.body.get().x1() - seat_data.x, dist_right: child.body.get().x1() - seat_data.x,
}, },
child, child,
@ -1067,10 +1428,13 @@ impl ContainerNode {
} }
} else { } else {
if seat_data.y < child.body.get().y1() { if seat_data.y < child.body.get().y1() {
let Some(prev) = child.prev() else {
continue;
};
break 'res ( break 'res (
SeatOpKind::Resize { SeatOpKind::Resize {
dist_left: seat_data.y dist_left: seat_data.y
- child.prev().unwrap().body.get().y2(), - prev.body.get().y2(),
dist_right: child.body.get().y1() - seat_data.y, dist_right: child.body.get().y1() - seat_data.y,
}, },
child, child,
@ -1285,6 +1649,78 @@ pub async fn container_render_positions(state: Rc<State>) {
} }
} }
pub async fn container_tab_render_textures(state: Rc<State>) {
loop {
let container = state.pending_container_tab_render_textures.pop().await;
container.update_tab_textures_scheduled.set(false);
let (event, textures) = container.update_tab_textures_phase1();
event.triggered().await;
container.update_tab_textures_phase2(&textures);
}
}
impl ContainerNode {
fn update_tab_textures_phase1(
self: &Rc<Self>,
) -> (Rc<crate::utils::asyncevent::AsyncEvent>, Vec<Rc<RefCell<Option<TextTexture>>>>) {
let on_completed = Rc::new(OnDropEvent::default());
let (entries, bar_height, render_scale) = {
let tab_bar = self.tab_bar.borrow();
let Some(tb) = tab_bar.as_ref() else {
return (on_completed.event(), vec![]);
};
let entries: Vec<_> = tb.entries.iter().map(|e| {
(e.title.clone(), TabBar::entry_colors(&self.state, e), e.title_texture.clone())
}).collect();
(entries, tb.height, tb.render_scale)
};
let Some(ctx) = self.state.render_ctx.get() else {
log::warn!("tab text phase1: no render context");
return (on_completed.event(), vec![]);
};
let font = self.state.theme.title_font();
let scale = if render_scale != Scale::from_int(1) {
Some(render_scale.to_f64())
} else {
None
};
let mut texture_height = bar_height;
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();
let tex = tex_ref.get_or_insert_with(|| TextTexture::new(&self.state, &ctx));
tex.schedule_render_fitting(
on_completed.clone(),
Some(texture_height),
&font,
title,
*text_color,
false,
scale,
);
texture_refs.push(title_texture.clone());
scheduled += 1;
}
(on_completed.event(), texture_refs)
}
fn update_tab_textures_phase2(&self, textures: &[Rc<RefCell<Option<TextTexture>>>]) {
for title_texture in textures {
let tex_ref = title_texture.borrow();
if let Some(tex) = tex_ref.as_ref() {
if let Err(e) = tex.flip() {
log::warn!("Could not render tab text: {}", ErrorFmt(e));
}
}
}
self.damage();
}
}
impl Node for ContainerNode { impl Node for ContainerNode {
fn node_id(&self) -> NodeId { fn node_id(&self) -> NodeId {
self.id.into() self.id.into()
@ -1329,8 +1765,9 @@ impl Node for ContainerNode {
self.toplevel_data.node_layer() self.toplevel_data.node_layer()
} }
fn node_child_title_changed(self: Rc<Self>, _child: &dyn Node, _title: &str) { fn node_child_title_changed(self: Rc<Self>, child: &dyn Node, title: &str) {
// Titlebars removed; no title tracking needed self.rebuild_tab_bar_with_override(Some(child.node_id()), Some(title));
self.damage();
} }
fn node_do_focus(self: Rc<Self>, seat: &Rc<WlSeatGlobal>, direction: Direction) { fn node_do_focus(self: Rc<Self>, seat: &Rc<WlSeatGlobal>, direction: Direction) {
@ -1433,8 +1870,31 @@ impl Node for ContainerNode {
self.button(id, seat, time_usec, state == ButtonState::Pressed, button); self.button(id, seat, time_usec, state == ButtonState::Pressed, button);
} }
fn node_on_axis_event(self: Rc<Self>, _seat: &Rc<WlSeatGlobal>, _event: &PendingScroll) { fn node_on_axis_event(self: Rc<Self>, _seat: &Rc<WlSeatGlobal>, event: &PendingScroll) {
// Scroll-to-switch-tabs was a title bar feature; no-op without titles if self.mono_child.is_none() {
return;
}
// Use vertical scroll (index 1) to switch tabs.
let v = match event.v120[1].get() {
Some(v) if v != 0 => v,
_ => return,
};
let mono = match self.mono_child.get() {
Some(m) => m,
None => return,
};
let next = if v > 0 {
// Scroll down → next tab.
mono.next().or_else(|| self.children.first())
} else {
// Scroll up → previous tab.
mono.prev().or_else(|| self.children.last())
};
if let Some(next) = next {
if next.node.node_id() != mono.node.node_id() {
self.activate_child(&next);
}
}
} }
fn node_on_leave(&self, seat: &WlSeatGlobal) { fn node_on_leave(&self, seat: &WlSeatGlobal) {
@ -1644,7 +2104,27 @@ impl ContainingNode for ContainerNode {
} }
} }
self.sum_factors.set(sum); self.sum_factors.set(sum);
// Ephemeral collapse: if this container is ephemeral and has exactly
// one child remaining, replace this container with that child in the parent.
if self.ephemeral.get() == Ephemeral::On
&& num_children == 1
&& !self.toplevel_data.is_fullscreen.get()
{
if let Some(parent) = self.toplevel_data.parent.get() {
if let Some(only_child) = self.children.first() {
let child_node = only_child.node.clone();
if parent.cnode_accepts_child(&*child_node) {
parent.cnode_replace_child(self.deref(), child_node);
self.toplevel_data.parent.take();
self.child_nodes.borrow_mut().clear();
self.tl_destroy();
return;
}
}
}
}
// log::info!("cnode_remove_child2"); // log::info!("cnode_remove_child2");
self.rebuild_tab_bar();
self.schedule_layout(); self.schedule_layout();
self.cancel_seat_ops(); self.cancel_seat_ops();
self.child_removed.trigger(); self.child_removed.trigger();
@ -1664,6 +2144,8 @@ impl ContainingNode for ContainerNode {
return; return;
} }
self.mod_attention_requests(set); self.mod_attention_requests(set);
drop(children);
self.rebuild_tab_bar();
self.schedule_compute_render_positions(); self.schedule_compute_render_positions();
} }
@ -1882,6 +2364,13 @@ impl ToplevelNodeBase for ContainerNode {
size_changed |= self.height.replace(rect.height()) != rect.height(); size_changed |= self.height.replace(rect.height()) != rect.height();
if size_changed { if size_changed {
self.update_content_size(); self.update_content_size();
// Re-layout tab bar entries when container size changes in mono mode.
if self.mono_child.is_some() {
if 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);
}
}
// log::info!("tl_change_extents"); // log::info!("tl_change_extents");
self.perform_layout(); self.perform_layout();
self.cancel_seat_ops(); self.cancel_seat_ops();

113
src/tree/tab_bar.rs Normal file
View file

@ -0,0 +1,113 @@
use {
crate::{
scale::Scale,
state::State,
text::TextTexture,
theme::Color,
tree::NodeId,
},
std::{cell::{Cell, RefCell}, rc::Rc},
};
/// A single entry (tab) within a tab bar.
pub struct TabBarEntry {
/// The node ID of the child this tab represents.
pub child_id: NodeId,
/// The display title of the tab.
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>,
}
/// A tab bar rendered above a container in mono (tabbed) mode.
pub struct TabBar {
/// The individual tab entries.
pub entries: Vec<TabBarEntry>,
/// Height of the tab bar in pixels (from theme).
pub height: i32,
/// The output scale at which text textures were rendered.
pub render_scale: Scale,
}
impl TabBar {
/// Create a new empty tab bar.
pub fn new(height: i32, render_scale: Scale) -> Self {
Self {
entries: Vec::new(),
height,
render_scale,
}
}
/// 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 {
return;
}
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 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 {
return Some(i);
}
}
None
}
/// Get the colors for a tab entry based on its state.
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(),
)
}
}
}

View file

@ -23,7 +23,7 @@ use {
}, },
ahash::AHashMap, ahash::AHashMap,
jay_config::{ jay_config::{
Axis, Direction, Workspace, Direction, Workspace,
client::ClientCapabilities, client::ClientCapabilities,
input::{ input::{
FallbackOutputMode, LayerDirection, SwitchEvent, Timeline, acceleration::AccelProfile, FallbackOutputMode, LayerDirection, SwitchEvent, Timeline, acceleration::AccelProfile,
@ -60,15 +60,10 @@ pub enum SimpleCommand {
Quit, Quit,
ReloadConfigSo, ReloadConfigSo,
ReloadConfigToml, ReloadConfigToml,
Split(Axis),
ToggleFloating, ToggleFloating,
SetFloating(bool), SetFloating(bool),
ToggleFullscreen, ToggleFullscreen,
SetFullscreen(bool), SetFullscreen(bool),
ToggleMono,
SetMono(bool),
ToggleSplit,
SetSplit(Axis),
Forward(bool), Forward(bool),
EnableWindowManagement(bool), EnableWindowManagement(bool),
SetFloatAboveFullscreen(bool), SetFloatAboveFullscreen(bool),
@ -94,6 +89,17 @@ pub enum SimpleCommand {
ReloadSimpleIm, ReloadSimpleIm,
EnableUnicodeInput, EnableUnicodeInput,
WarpMouseToFocus, WarpMouseToFocus,
ToggleTab,
MakeGroupH,
MakeGroupV,
MakeGroupTab,
ChangeGroupOpposite,
Equalize,
EqualizeRecursive,
MoveTabLeft,
MoveTabRight,
SetAutotile(bool),
ToggleAutotile,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -229,6 +235,21 @@ pub struct Theme {
pub floating_titles: Option<bool>, pub floating_titles: Option<bool>,
pub title_gap: Option<i32>, pub title_gap: Option<i32>,
pub corner_radius: Option<f32>, pub corner_radius: Option<f32>,
pub tab_active_bg_color: Option<Color>,
pub tab_active_border_color: Option<Color>,
pub tab_inactive_bg_color: Option<Color>,
pub tab_inactive_border_color: Option<Color>,
pub tab_active_text_color: Option<Color>,
pub tab_inactive_text_color: Option<Color>,
pub tab_bar_bg_color: Option<Color>,
pub tab_attention_bg_color: Option<Color>,
pub tab_bar_height: Option<i32>,
pub tab_bar_padding: Option<i32>,
pub tab_bar_radius: Option<i32>,
pub tab_bar_border_width: Option<i32>,
pub tab_bar_text_padding: Option<i32>,
pub tab_bar_gap: Option<i32>,
pub tab_title_align: Option<String>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View file

@ -33,7 +33,6 @@ use {
}, },
indexmap::IndexMap, indexmap::IndexMap,
jay_config::{ jay_config::{
Axis::{Horizontal, Vertical},
Direction, get_workspace, Direction, get_workspace,
input::{LayerDirection, Timeline}, input::{LayerDirection, Timeline},
}, },
@ -115,14 +114,6 @@ impl ActionParser<'_> {
"move-down" => Move(Down), "move-down" => Move(Down),
"move-up" => Move(Up), "move-up" => Move(Up),
"move-right" => Move(Right), "move-right" => Move(Right),
"split-horizontal" => Split(Horizontal),
"split-vertical" => Split(Vertical),
"toggle-split" => ToggleSplit,
"tile-horizontal" => SetSplit(Horizontal),
"tile-vertical" => SetSplit(Vertical),
"toggle-mono" => ToggleMono,
"show-single" => SetMono(true),
"show-all" => SetMono(false),
"toggle-fullscreen" => ToggleFullscreen, "toggle-fullscreen" => ToggleFullscreen,
"enter-fullscreen" => SetFullscreen(true), "enter-fullscreen" => SetFullscreen(true),
"exit-fullscreen" => SetFullscreen(false), "exit-fullscreen" => SetFullscreen(false),
@ -172,6 +163,18 @@ impl ActionParser<'_> {
"reload-simple-im" => ReloadSimpleIm, "reload-simple-im" => ReloadSimpleIm,
"enable-unicode-input" => EnableUnicodeInput, "enable-unicode-input" => EnableUnicodeInput,
"warp-mouse-to-focus" => WarpMouseToFocus, "warp-mouse-to-focus" => WarpMouseToFocus,
"toggle-tab" => ToggleTab,
"make-group-h" => MakeGroupH,
"make-group-v" => MakeGroupV,
"make-group-tab" => MakeGroupTab,
"change-group-opposite" => ChangeGroupOpposite,
"equalize" => Equalize,
"equalize-recursive" => EqualizeRecursive,
"move-tab-left" => MoveTabLeft,
"move-tab-right" => MoveTabRight,
"enable-autotile" => SetAutotile(true),
"disable-autotile" => SetAutotile(false),
"toggle-autotile" => ToggleAutotile,
_ => { _ => {
return Err( return Err(
ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span) ActionParserError::UnknownSimpleAction(string.to_string()).spanned(span)

View file

@ -101,6 +101,41 @@ impl Parser for ThemeParser<'_> {
recover(opt(s32("title-gap"))), recover(opt(s32("title-gap"))),
recover(opt(fltorint("corner-radius"))), recover(opt(fltorint("corner-radius"))),
))?; ))?;
let (
(
tab_active_bg_color,
tab_active_border_color,
tab_inactive_bg_color,
tab_inactive_border_color,
tab_active_text_color,
tab_inactive_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),
) = ext.extract((
(
opt(val("tab-active-bg-color")),
opt(val("tab-active-border-color")),
opt(val("tab-inactive-bg-color")),
opt(val("tab-inactive-border-color")),
opt(val("tab-active-text-color")),
opt(val("tab-inactive-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(str("tab-title-align"))),
),
))?;
macro_rules! color { macro_rules! color {
($e:expr) => { ($e:expr) => {
match $e { match $e {
@ -156,6 +191,21 @@ impl Parser for ThemeParser<'_> {
floating_titles: floating_titles.despan(), floating_titles: floating_titles.despan(),
title_gap: title_gap.despan(), title_gap: title_gap.despan(),
corner_radius: corner_radius.map(|v| v.value as f32), corner_radius: corner_radius.map(|v| v.value as f32),
tab_active_bg_color: color!(tab_active_bg_color),
tab_active_border_color: color!(tab_active_border_color),
tab_inactive_bg_color: color!(tab_inactive_bg_color),
tab_inactive_border_color: color!(tab_inactive_border_color),
tab_active_text_color: color!(tab_active_text_color),
tab_inactive_text_color: color!(tab_inactive_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(),
tab_bar_padding: tab_bar_padding.despan(),
tab_bar_radius: tab_bar_radius.despan(),
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_title_align: tab_title_align_val.map(|v| v.value.to_string()),
}) })
} }
} }

View file

@ -23,6 +23,7 @@ use {
ahash::{AHashMap, AHashSet}, ahash::{AHashMap, AHashSet},
error_reporter::Report, error_reporter::Report,
jay_config::{ jay_config::{
Axis,
client::Client, client::Client,
config, config_dir, config, config_dir,
exec::{Command, set_env, unset_env}, exec::{Command, set_env, unset_env},
@ -40,7 +41,7 @@ use {
set_color_management_enabled, set_default_workspace_capture, set_explicit_sync_enabled, set_color_management_enabled, set_default_workspace_capture, set_explicit_sync_enabled,
set_float_above_fullscreen, set_idle, set_idle_grace_period, set_float_above_fullscreen, set_idle, set_idle_grace_period,
set_floating_titles, set_middle_click_paste_enabled, set_show_bar, set_show_float_pin_icon, set_floating_titles, set_middle_click_paste_enabled, set_show_bar, set_show_float_pin_icon,
set_show_titles, set_corner_radius, set_show_titles, set_corner_radius, set_autotile, set_tab_title_align,
set_ui_drag_enabled, set_ui_drag_threshold, set_ui_drag_enabled, set_ui_drag_threshold,
status::{set_i3bar_separator, set_status, set_status_command, unset_status_command}, status::{set_i3bar_separator, set_status, set_status_command, unset_status_command},
switch_to_vt, switch_to_vt,
@ -169,11 +170,6 @@ impl Action {
Action::SimpleCommand { cmd } => match cmd { Action::SimpleCommand { cmd } => match cmd {
SimpleCommand::Focus(dir) => b.new(move || s.focus(dir)), SimpleCommand::Focus(dir) => b.new(move || s.focus(dir)),
SimpleCommand::Move(dir) => window_or_seat!(s, s.move_(dir)), SimpleCommand::Move(dir) => window_or_seat!(s, s.move_(dir)),
SimpleCommand::Split(axis) => window_or_seat!(s, s.create_split(axis)),
SimpleCommand::ToggleSplit => window_or_seat!(s, s.toggle_split()),
SimpleCommand::SetSplit(b) => window_or_seat!(s, s.set_split(b)),
SimpleCommand::ToggleMono => window_or_seat!(s, s.toggle_mono()),
SimpleCommand::SetMono(b) => window_or_seat!(s, s.set_mono(b)),
SimpleCommand::ToggleFullscreen => window_or_seat!(s, s.toggle_fullscreen()), SimpleCommand::ToggleFullscreen => window_or_seat!(s, s.toggle_fullscreen()),
SimpleCommand::SetFullscreen(b) => window_or_seat!(s, s.set_fullscreen(b)), SimpleCommand::SetFullscreen(b) => window_or_seat!(s, s.set_fullscreen(b)),
SimpleCommand::FocusParent => b.new(move || s.focus_parent()), SimpleCommand::FocusParent => b.new(move || s.focus_parent()),
@ -259,6 +255,35 @@ impl Action {
let persistent = state.persistent.clone(); let persistent = state.persistent.clone();
b.new(move || persistent.seat.warp_mouse_to_focus()) b.new(move || persistent.seat.warp_mouse_to_focus())
} }
SimpleCommand::ToggleTab => b.new(move || s.toggle_tab()),
SimpleCommand::MakeGroupH => {
b.new(move || s.make_group(Axis::Horizontal, true))
}
SimpleCommand::MakeGroupV => {
b.new(move || s.make_group(Axis::Vertical, true))
}
SimpleCommand::MakeGroupTab => {
b.new(move || {
s.make_group(Axis::Horizontal, true);
s.toggle_tab();
})
}
SimpleCommand::ChangeGroupOpposite => {
b.new(move || s.change_group_opposite())
}
SimpleCommand::Equalize => b.new(move || s.equalize(false)),
SimpleCommand::EqualizeRecursive => b.new(move || s.equalize(true)),
SimpleCommand::MoveTabLeft => b.new(move || s.move_tab(false)),
SimpleCommand::MoveTabRight => b.new(move || s.move_tab(true)),
SimpleCommand::SetAutotile(enabled) => {
b.new(move || set_autotile(enabled))
}
SimpleCommand::ToggleAutotile => {
b.new(move || {
// Toggle not directly supported; set to true
set_autotile(true)
})
}
}, },
Action::Multi { actions } => { Action::Multi { actions } => {
let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect(); let actions: Vec<_> = actions.into_iter().map(|a| a.into_fn(state)).collect();
@ -997,6 +1022,14 @@ impl State {
color!(UNFOCUSED_TITLE_BACKGROUND_COLOR, unfocused_title_bg_color); color!(UNFOCUSED_TITLE_BACKGROUND_COLOR, unfocused_title_bg_color);
color!(UNFOCUSED_TITLE_TEXT_COLOR, unfocused_title_text_color); color!(UNFOCUSED_TITLE_TEXT_COLOR, unfocused_title_text_color);
color!(HIGHLIGHT_COLOR, highlight_color); 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_INACTIVE_BACKGROUND_COLOR, tab_inactive_bg_color);
color!(TAB_INACTIVE_BORDER_COLOR, tab_inactive_border_color);
color!(TAB_ACTIVE_TEXT_COLOR, tab_active_text_color);
color!(TAB_INACTIVE_TEXT_COLOR, tab_inactive_text_color);
color!(TAB_BAR_BACKGROUND_COLOR, tab_bar_bg_color);
color!(TAB_ATTENTION_BACKGROUND_COLOR, tab_attention_bg_color);
macro_rules! size { macro_rules! size {
($sized:ident, $field:ident) => { ($sized:ident, $field:ident) => {
if let Some(size) = theme.$field { if let Some(size) = theme.$field {
@ -1010,6 +1043,12 @@ impl State {
size!(BAR_SEPARATOR_WIDTH, bar_separator_width); size!(BAR_SEPARATOR_WIDTH, bar_separator_width);
size!(GAP, gap); size!(GAP, gap);
size!(TITLE_GAP, title_gap); size!(TITLE_GAP, title_gap);
size!(TAB_BAR_HEIGHT, tab_bar_height);
size!(TAB_BAR_PADDING, tab_bar_padding);
size!(TAB_BAR_RADIUS, tab_bar_radius);
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);
macro_rules! font { macro_rules! font {
($fun:ident, $field:ident) => { ($fun:ident, $field:ident) => {
if let Some(font) = &theme.$field { if let Some(font) = &theme.$field {
@ -1023,6 +1062,9 @@ impl State {
if let Some(radius) = theme.corner_radius { if let Some(radius) = theme.corner_radius {
set_corner_radius(radius); set_corner_radius(radius);
} }
if let Some(ref align) = theme.tab_title_align {
set_tab_title_align(align);
}
} }
fn handle_switch_device(self: &Rc<Self>, dev: InputDevice, actions: &Rc<SwitchActions>) { fn handle_switch_device(self: &Rc<Self>, dev: InputDevice, actions: &Rc<SwitchActions>) {