all: add support for hy3 like tiling
This commit is contained in:
parent
a41dbae899
commit
cea4187fc0
21 changed files with 1237 additions and 48 deletions
|
|
@ -62,6 +62,7 @@ use {
|
|||
WorkspaceDisplayOrder, WorkspaceNode, container_layout, container_render_positions,
|
||||
float_layout, output_render_data,
|
||||
placeholder_render_textures,
|
||||
container_tab_render_textures,
|
||||
},
|
||||
user_session::import_environment,
|
||||
utils::{
|
||||
|
|
@ -265,6 +266,7 @@ fn start_compositor2(
|
|||
pending_toplevel_screencasts: Default::default(),
|
||||
pending_screencast_reallocs_or_reconfigures: Default::default(),
|
||||
pending_placeholder_render_textures: Default::default(),
|
||||
pending_container_tab_render_textures: Default::default(),
|
||||
dbus: Dbus::new(&engine, &ring, &run_toplevel),
|
||||
fdcloser: FdCloser::new(),
|
||||
logger: logger.clone(),
|
||||
|
|
@ -514,6 +516,11 @@ fn start_global_event_handlers(state: &Rc<State>) -> Vec<SpawnedFuture<()>> {
|
|||
Phase::PostLayout,
|
||||
placeholder_render_textures(state.clone()),
|
||||
),
|
||||
eng.spawn2(
|
||||
"container tab textures",
|
||||
Phase::PostLayout,
|
||||
container_tab_render_textures(state.clone()),
|
||||
),
|
||||
eng.spawn2(
|
||||
"output render",
|
||||
Phase::PostLayout,
|
||||
|
|
|
|||
|
|
@ -1757,6 +1757,41 @@ impl ConfigProxyHandler {
|
|||
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> {
|
||||
let window = self.get_window(window)?;
|
||||
self.respond(Response::GetWindowSplit {
|
||||
|
|
@ -2464,6 +2499,12 @@ impl ConfigProxyHandler {
|
|||
BAR_SEPARATOR_WIDTH => ThemeSized::bar_separator_width,
|
||||
GAP => ThemeSized::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)),
|
||||
};
|
||||
Ok(sized)
|
||||
|
|
@ -2541,6 +2582,14 @@ impl ConfigProxyHandler {
|
|||
BAR_STATUS_TEXT_COLOR => ThemeColor::bar_text,
|
||||
ATTENTION_REQUESTED_BACKGROUND_COLOR => ThemeColor::attention_requested_background,
|
||||
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)),
|
||||
};
|
||||
Ok(colorable)
|
||||
|
|
@ -3448,6 +3497,40 @@ impl ConfigProxyHandler {
|
|||
} => self
|
||||
.handle_window_resize(window, dx1, dy1, dx2, dy2)
|
||||
.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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -306,6 +306,8 @@ pub struct RoundedFillRect {
|
|||
pub border_width: f32,
|
||||
/// Output scale for antialiasing.
|
||||
pub scale: f32,
|
||||
/// Sort order hint within the RoundedFill bucket (lower renders first).
|
||||
pub z_order: u32,
|
||||
}
|
||||
|
||||
impl RoundedFillRect {
|
||||
|
|
|
|||
|
|
@ -254,6 +254,7 @@ struct VulkanRoundedFillOp {
|
|||
border_width: f32,
|
||||
scale: f32,
|
||||
range_address: DeviceAddress,
|
||||
z_order: u32,
|
||||
}
|
||||
|
||||
struct VulkanRoundedTexOp {
|
||||
|
|
@ -923,7 +924,7 @@ impl VulkanRenderer {
|
|||
enum Key {
|
||||
Fill { color: [u32; 4] },
|
||||
Tex(usize),
|
||||
RoundedFill { color: [u32; 4] },
|
||||
RoundedFill { z_order: u32, color: [u32; 4] },
|
||||
RoundedTex(usize),
|
||||
}
|
||||
match o {
|
||||
|
|
@ -932,6 +933,7 @@ impl VulkanRenderer {
|
|||
},
|
||||
VulkanOp::Tex(t) => Key::Tex(t.index),
|
||||
VulkanOp::RoundedFill(f) => Key::RoundedFill {
|
||||
z_order: f.z_order,
|
||||
color: f.color.map(|c| c.to_bits()),
|
||||
},
|
||||
VulkanOp::RoundedTex(t) => Key::RoundedTex(t.index),
|
||||
|
|
@ -1152,6 +1154,7 @@ impl VulkanRenderer {
|
|||
border_width: rf.border_width,
|
||||
scale: rf.scale,
|
||||
range_address: 0,
|
||||
z_order: rf.z_order,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -2385,12 +2388,13 @@ impl VulkanRenderer {
|
|||
};
|
||||
(opaque, c.target)
|
||||
}
|
||||
GfxApiOpt::RoundedFillRect(_) => {
|
||||
// Rounded rects are never fully opaque due to AA at corners
|
||||
continue;
|
||||
GfxApiOpt::RoundedFillRect(rf) => {
|
||||
// Rounded rects are never fully opaque due to AA at corners,
|
||||
// but they do paint pixels and need paint regions.
|
||||
(false, rf.rect)
|
||||
}
|
||||
GfxApiOpt::RoundedCopyTexture(_) => {
|
||||
continue;
|
||||
GfxApiOpt::RoundedCopyTexture(ct) => {
|
||||
(false, ct.target)
|
||||
}
|
||||
};
|
||||
if opaque || bb.is_none() {
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ use {
|
|||
rect::Rect,
|
||||
state::{DeviceHandlerData, State},
|
||||
tree::{
|
||||
ContainerNode, ContainerSplit, Direction, FoundNode, Node, NodeId, NodeLayer,
|
||||
ContainerNode, ContainerSplit, ChangeGroupAction, Direction, FoundNode, Node, NodeId, NodeLayer,
|
||||
NodeLayerLink, NodeLocation, OutputNode, StackedNode, ToplevelNode, WorkspaceNode,
|
||||
generic_node_visitor, toplevel_create_split, toplevel_parent_container,
|
||||
toplevel_set_floating, toplevel_set_workspace,
|
||||
|
|
@ -745,6 +745,40 @@ impl WlSeatGlobal {
|
|||
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>) {
|
||||
if let Some(tl) = self.keyboard_node.get().node_toplevel()
|
||||
&& let Some(parent) = tl.tl_data().parent.get()
|
||||
|
|
|
|||
123
src/renderer.rs
123
src/renderer.rs
|
|
@ -15,7 +15,7 @@ use {
|
|||
theme::{Color, CornerRadius},
|
||||
tree::{
|
||||
ContainerNode, DisplayNode, FloatNode, OutputNode, PlaceholderNode, ToplevelData,
|
||||
ToplevelNodeBase, WorkspaceNode,
|
||||
ToplevelNodeBase, WorkspaceNode, tab_bar::TabBar,
|
||||
},
|
||||
},
|
||||
std::{ops::Deref, rc::Rc, slice},
|
||||
|
|
@ -277,6 +277,105 @@ impl Renderer<'_> {
|
|||
self.render_tl_aux(placeholder.tl_data(), bounds, true);
|
||||
}
|
||||
|
||||
fn render_tab_bar(&mut self, tab_bar: &TabBar, x: i32, y: i32, _container_width: i32) {
|
||||
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) {
|
||||
let srgb_srgb = self.state.color_manager.srgb_gamma22();
|
||||
let srgb = &srgb_srgb.linear;
|
||||
|
|
@ -291,6 +390,13 @@ impl Renderer<'_> {
|
|||
self.render_container_decorations(container, x, y);
|
||||
|
||||
if let Some(child) = container.mono_child.get() {
|
||||
// Render tab bar if present.
|
||||
{
|
||||
let tab_bar = container.tab_bar.borrow();
|
||||
if let Some(tb) = tab_bar.as_ref() {
|
||||
self.render_tab_bar(tb, x, y, container.width.get());
|
||||
}
|
||||
}
|
||||
let mb = container.mono_body.get();
|
||||
if self.state.theme.sizes.gap.get() != 0 {
|
||||
let srgb_srgb = self.state.color_manager.srgb_gamma22();
|
||||
|
|
@ -308,21 +414,22 @@ impl Renderer<'_> {
|
|||
let perceptual = RenderIntent::Perceptual;
|
||||
if !child.node.node_is_container() {
|
||||
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() {
|
||||
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),
|
||||
Rect::new_sized_saturating(mb.x1() - bw, frame_y, bw, frame_h),
|
||||
Rect::new_sized_saturating(mb.x2(), frame_y, bw, frame_h),
|
||||
Rect::new_sized_saturating(mb.x1() - bw, frame_y - bw, 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);
|
||||
} else {
|
||||
let outer = Rect::new_sized_saturating(
|
||||
mb.x1() - bw,
|
||||
-bw,
|
||||
frame_y - bw,
|
||||
full_w + 2 * bw,
|
||||
full_h + 2 * bw,
|
||||
frame_h + 2 * bw,
|
||||
);
|
||||
let scalef = self.base.scalef as f32;
|
||||
let scaled_cr = cr.scaled_by(scalef);
|
||||
|
|
|
|||
|
|
@ -259,6 +259,20 @@ impl RendererBase<'_> {
|
|||
render_intent: RenderIntent,
|
||||
corner_radius: CornerRadius,
|
||||
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 {
|
||||
return;
|
||||
|
|
@ -288,6 +302,7 @@ impl RendererBase<'_> {
|
|||
corner_radius: cr,
|
||||
border_width,
|
||||
scale,
|
||||
z_order,
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -195,6 +195,7 @@ pub struct State {
|
|||
pub pending_toplevel_screencasts: AsyncQueue<Rc<JayScreencast>>,
|
||||
pub pending_screencast_reallocs_or_reconfigures: AsyncQueue<Rc<JayScreencast>>,
|
||||
pub pending_placeholder_render_textures: AsyncQueue<Rc<PlaceholderNode>>,
|
||||
pub pending_container_tab_render_textures: AsyncQueue<Rc<ContainerNode>>,
|
||||
pub dbus: Dbus,
|
||||
pub fdcloser: Arc<FdCloser>,
|
||||
pub logger: Option<Arc<Logger>>,
|
||||
|
|
@ -1112,6 +1113,7 @@ impl State {
|
|||
self.pending_toplevel_screencasts.clear();
|
||||
self.pending_screencast_reallocs_or_reconfigures.clear();
|
||||
self.pending_placeholder_render_textures.clear();
|
||||
self.pending_container_tab_render_textures.clear();
|
||||
self.render_ctx_watchers.clear();
|
||||
self.workspace_watchers.clear();
|
||||
self.toplevel_lists.clear();
|
||||
|
|
|
|||
45
src/theme.rs
45
src/theme.rs
|
|
@ -454,6 +454,14 @@ colors! {
|
|||
bar_text = (0xff, 0xff, 0xff),
|
||||
attention_requested_background = (0x23, 0x09, 0x2c),
|
||||
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 {
|
||||
|
|
@ -476,6 +484,14 @@ impl StaticText for ThemeColor {
|
|||
ThemeColor::bar_text => "Bar Text",
|
||||
ThemeColor::attention_requested_background => "Attention Requested",
|
||||
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),
|
||||
gap = (0, 1000, 0),
|
||||
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 {
|
||||
|
|
@ -599,6 +621,12 @@ impl StaticText for ThemeSized {
|
|||
ThemeSized::bar_separator_width => "Bar Separator Width",
|
||||
ThemeSized::gap => "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 colors: ThemeColors,
|
||||
pub sizes: ThemeSizes,
|
||||
|
|
@ -745,6 +782,8 @@ pub struct Theme {
|
|||
pub floating_titles: Cell<bool>,
|
||||
pub bar_position: Cell<BarPosition>,
|
||||
pub corner_radius: Cell<CornerRadius>,
|
||||
pub autotile_enabled: Cell<bool>,
|
||||
pub tab_title_align: Cell<TabTitleAlign>,
|
||||
}
|
||||
|
||||
impl Default for Theme {
|
||||
|
|
@ -761,6 +800,8 @@ impl Default for Theme {
|
|||
floating_titles: Cell::new(false),
|
||||
bar_position: Default::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())
|
||||
}
|
||||
|
||||
pub fn title_font(&self) -> Arc<String> {
|
||||
self.title_font.get().unwrap_or_else(|| self.font.get())
|
||||
}
|
||||
|
||||
pub fn title_height(&self) -> i32 {
|
||||
0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ mod float;
|
|||
mod output;
|
||||
mod placeholder;
|
||||
mod stacked;
|
||||
pub mod tab_bar;
|
||||
mod toplevel;
|
||||
mod walker;
|
||||
mod workspace;
|
||||
|
|
|
|||
|
|
@ -12,20 +12,24 @@ use {
|
|||
},
|
||||
rect::Rect,
|
||||
renderer::Renderer,
|
||||
scale::Scale,
|
||||
state::State,
|
||||
text::TextTexture,
|
||||
tree::{
|
||||
ContainingNode, Direction, FindTreeResult, FindTreeUsecase, FloatNode, FoundNode, Node,
|
||||
NodeId, NodeLayerLink, NodeLocation, OutputNode, TddType, TileDragDestination,
|
||||
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,
|
||||
},
|
||||
utils::{
|
||||
clonecell::CloneCell,
|
||||
errorfmt::ErrorFmt,
|
||||
event_listener::LazyEventSource,
|
||||
hash_map_ext::HashMapExt,
|
||||
linkedlist::{LinkedList, LinkedNode, NodeRef},
|
||||
numcell::NumCell,
|
||||
on_drop_event::OnDropEvent,
|
||||
rc_eq::rc_eq,
|
||||
threshold_counter::ThresholdCounter,
|
||||
},
|
||||
|
|
@ -85,6 +89,28 @@ pub enum ContainerFocus {
|
|||
|
||||
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)]
|
||||
pub struct ContainerRenderData {
|
||||
pub border_rects: Vec<Rect>,
|
||||
|
|
@ -120,6 +146,9 @@ pub struct ContainerNode {
|
|||
pub child_added: Rc<LazyEventSource>,
|
||||
pub child_removed: 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 {
|
||||
|
|
@ -231,6 +260,9 @@ impl ContainerNode {
|
|||
child_added: state.lazy_event_sources.create_source(),
|
||||
child_removed: state.lazy_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());
|
||||
slf.pull_child_properties(&child_node_ref);
|
||||
|
|
@ -335,6 +367,8 @@ impl ContainerNode {
|
|||
self.sum_factors.set(sum_factors);
|
||||
if self.mono_child.is_some() {
|
||||
self.activate_child(&new_ref);
|
||||
self.rebuild_tab_bar();
|
||||
self.damage();
|
||||
}
|
||||
// log::info!("add_child");
|
||||
self.schedule_layout();
|
||||
|
|
@ -549,11 +583,19 @@ impl ContainerNode {
|
|||
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(
|
||||
0,
|
||||
0,
|
||||
tab_bar_height,
|
||||
self.width.get(),
|
||||
self.height.get(),
|
||||
(self.height.get() - tab_bar_height).max(0),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -588,7 +630,9 @@ impl ContainerNode {
|
|||
dist_left,
|
||||
dist_right,
|
||||
} => {
|
||||
let prev = op.child.prev().unwrap();
|
||||
let Some(prev) = op.child.prev() else {
|
||||
return;
|
||||
};
|
||||
let prev_body = prev.body.get();
|
||||
let child_body = op.child.body.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) {
|
||||
self.compute_render_positions_scheduled.set(false);
|
||||
let mut rd = self.render_data.borrow_mut();
|
||||
|
|
@ -713,6 +765,13 @@ impl ContainerNode {
|
|||
}
|
||||
}
|
||||
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() {
|
||||
self.perform_layout();
|
||||
child.node.tl_set_visible(true);
|
||||
|
|
@ -764,9 +823,25 @@ impl ContainerNode {
|
|||
}
|
||||
}
|
||||
}
|
||||
self.mono_child.set(child);
|
||||
// log::info!("set_mono");
|
||||
self.mono_child.set(child.clone());
|
||||
if child.is_some() {
|
||||
self.rebuild_tab_bar();
|
||||
} else {
|
||||
*self.tab_bar.borrow_mut() = None;
|
||||
}
|
||||
self.update_content_size();
|
||||
self.damage();
|
||||
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) {
|
||||
|
|
@ -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>> {
|
||||
self.toplevel_data
|
||||
.parent
|
||||
|
|
@ -955,6 +1257,42 @@ impl ContainerNode {
|
|||
}
|
||||
|
||||
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);
|
||||
if split != self.split.get() || right {
|
||||
self.append_child(node);
|
||||
|
|
@ -1050,16 +1388,39 @@ impl ContainerNode {
|
|||
if !pressed {
|
||||
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 mono = self.mono_child.is_some();
|
||||
for child in self.children.iter() {
|
||||
if !mono {
|
||||
if self.split.get() == ContainerSplit::Horizontal {
|
||||
if seat_data.x < child.body.get().x1() {
|
||||
let Some(prev) = child.prev() else {
|
||||
continue;
|
||||
};
|
||||
break 'res (
|
||||
SeatOpKind::Resize {
|
||||
dist_left: seat_data.x
|
||||
- child.prev().unwrap().body.get().x2(),
|
||||
- prev.body.get().x2(),
|
||||
dist_right: child.body.get().x1() - seat_data.x,
|
||||
},
|
||||
child,
|
||||
|
|
@ -1067,10 +1428,13 @@ impl ContainerNode {
|
|||
}
|
||||
} else {
|
||||
if seat_data.y < child.body.get().y1() {
|
||||
let Some(prev) = child.prev() else {
|
||||
continue;
|
||||
};
|
||||
break 'res (
|
||||
SeatOpKind::Resize {
|
||||
dist_left: seat_data.y
|
||||
- child.prev().unwrap().body.get().y2(),
|
||||
- prev.body.get().y2(),
|
||||
dist_right: child.body.get().y1() - seat_data.y,
|
||||
},
|
||||
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 {
|
||||
fn node_id(&self) -> NodeId {
|
||||
self.id.into()
|
||||
|
|
@ -1329,8 +1765,9 @@ impl Node for ContainerNode {
|
|||
self.toplevel_data.node_layer()
|
||||
}
|
||||
|
||||
fn node_child_title_changed(self: Rc<Self>, _child: &dyn Node, _title: &str) {
|
||||
// Titlebars removed; no title tracking needed
|
||||
fn node_child_title_changed(self: Rc<Self>, child: &dyn Node, title: &str) {
|
||||
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) {
|
||||
|
|
@ -1433,8 +1870,31 @@ impl Node for ContainerNode {
|
|||
self.button(id, seat, time_usec, state == ButtonState::Pressed, button);
|
||||
}
|
||||
|
||||
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
|
||||
fn node_on_axis_event(self: Rc<Self>, _seat: &Rc<WlSeatGlobal>, event: &PendingScroll) {
|
||||
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) {
|
||||
|
|
@ -1644,7 +2104,27 @@ impl ContainingNode for ContainerNode {
|
|||
}
|
||||
}
|
||||
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");
|
||||
self.rebuild_tab_bar();
|
||||
self.schedule_layout();
|
||||
self.cancel_seat_ops();
|
||||
self.child_removed.trigger();
|
||||
|
|
@ -1664,6 +2144,8 @@ impl ContainingNode for ContainerNode {
|
|||
return;
|
||||
}
|
||||
self.mod_attention_requests(set);
|
||||
drop(children);
|
||||
self.rebuild_tab_bar();
|
||||
self.schedule_compute_render_positions();
|
||||
}
|
||||
|
||||
|
|
@ -1882,6 +2364,13 @@ impl ToplevelNodeBase for ContainerNode {
|
|||
size_changed |= self.height.replace(rect.height()) != rect.height();
|
||||
if size_changed {
|
||||
self.update_content_size();
|
||||
// Re-layout tab bar entries when container size changes in mono mode.
|
||||
if self.mono_child.is_some() {
|
||||
if let Some(bar) = self.tab_bar.borrow().as_ref() {
|
||||
let padding = self.state.theme.sizes.tab_bar_padding.get();
|
||||
bar.layout_entries(rect.width(), padding);
|
||||
}
|
||||
}
|
||||
// log::info!("tl_change_extents");
|
||||
self.perform_layout();
|
||||
self.cancel_seat_ops();
|
||||
|
|
|
|||
113
src/tree/tab_bar.rs
Normal file
113
src/tree/tab_bar.rs
Normal 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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue