feat(tile/tabs): display tab window titles

This commit is contained in:
2025-11-25 12:47:42 +01:00
parent 4141c8d3da
commit 614c182bac
11 changed files with 429 additions and 71 deletions

12
Cargo.lock generated
View File

@@ -443,7 +443,7 @@ dependencies = [
"bitflags 2.9.4",
"cexpr",
"clang-sys",
"itertools",
"itertools 0.13.0",
"proc-macro2",
"quote",
"regex",
@@ -1927,6 +1927,15 @@ dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.15"
@@ -2358,6 +2367,7 @@ dependencies = [
"glam",
"input",
"insta",
"itertools 0.14.0",
"keyframe",
"libc",
"libdisplay-info",

View File

@@ -109,6 +109,7 @@ wayland-backend = "0.3.11"
wayland-scanner = "0.31.7"
xcursor = "0.3.10"
zbus = { version = "5.11.0", optional = true }
itertools = "0.14.0"
[dependencies.smithay]
workspace = true

View File

@@ -1,4 +1,8 @@
# niri
# niri (fork)
<p align="center">
<img height="600px" src="assets/screenshots/groups-blur.png" />
</p>
This repo houses a fork of [niri](https://github/com/YaLTeR/niri), a scrollable tiling Wayland compositor.
@@ -111,6 +115,21 @@ When using `move-window-into-or-out-of-group` on a non-grouped tile, but there i
direction you're attempting to move to, the behavior will instead be similar to `consume-or-expel-window`, `-left` or
`-right` respectively, or `move-window`, `-up` or `-down` respectively.
By default, tab titles will be rendered above tab bars. Their appearance can be adjusted under the `tab-indicator`
setting:
```kdl
layout {
tab-indicator {
// default is 12
title-font-size 18
// optional, if you don't want titles to show at all
hide-titles
}
}
```
#### Caveats
- When maximizing or fullscreening a grouped tile, the tab indicator will disappear. However, you can still cycle tabs

Binary file not shown.

View File

@@ -489,6 +489,8 @@ impl MergeWith<WorkspaceShadowPart> for WorkspaceShadow {
pub struct TabIndicator {
pub off: bool,
pub hide_when_single_tab: bool,
pub hide_titles: bool,
pub title_font_size: u32,
pub gap: f64,
pub width: f64,
pub length: TabIndicatorLength,
@@ -508,6 +510,8 @@ impl Default for TabIndicator {
Self {
off: false,
hide_when_single_tab: false,
hide_titles: false,
title_font_size: 12,
gap: 5.,
width: 4.,
length: TabIndicatorLength {
@@ -535,6 +539,7 @@ impl MergeWith<TabIndicatorPart> for TabIndicator {
merge!(
(self, part),
hide_titles,
hide_when_single_tab,
gap,
width,
@@ -542,7 +547,7 @@ impl MergeWith<TabIndicatorPart> for TabIndicator {
corner_radius,
);
merge_clone!((self, part), length, position);
merge_clone!((self, part), title_font_size, length, position);
merge_color_gradient_opt!(
(self, part),
@@ -560,6 +565,10 @@ pub struct TabIndicatorPart {
#[knuffel(child)]
pub on: bool,
#[knuffel(child)]
pub hide_titles: Option<Flag>,
#[knuffel(child, unwrap(argument))]
pub title_font_size: Option<u32>,
#[knuffel(child)]
pub hide_when_single_tab: Option<Flag>,
#[knuffel(child, unwrap(argument))]
pub gap: Option<FloatOrInt<-65535, 65535>>,

View File

@@ -286,6 +286,11 @@ pub trait LayoutElement {
fn interactive_resize_data(&self) -> Option<InteractiveResizeData>;
fn on_commit(&mut self, serial: Serial);
/// The name / title of this layout element
fn title(&self) -> Option<String> {
None
}
}
#[derive(Debug)]

View File

@@ -1931,7 +1931,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
let source_tile = &mut self.columns[source_col_idx].tiles[source_tile_idx];
if let Some(window) = source_tile.ungroup_single(&id) {
let extra_offset = Point::new(0., source_tile.tab_indicator_extra_size().h);
let extra_offset = source_tile.tab_indicator_content_offset();
let to_update = source_tile.focused_window().id().clone();
// we need to refresh the window here to avoid triggering its resize animation
@@ -1979,14 +1979,21 @@ impl<W: LayoutElement> ScrollingSpace<W> {
Some(self.options.animations.window_movement.0),
);
let source_position = self
.tile_render_pos(source_col_idx + 1, source_tile_idx)
.expect("should determine source tile render pos");
let target_position = self
.tile_render_pos(source_col_idx, 0)
.expect("should determine source tile render pos");
let inserted_tile = self.columns[source_col_idx]
.tiles
.first_mut()
.expect("should have first tile");
inserted_tile.animate_move_from(Point::new(
inserted_tile.tile_bounding_box().w + self.options.layout.gaps,
extra_offset.y,
));
inserted_tile.animate_move_from(
source_position - target_position + extra_offset,
);
inserted_tile.focused_window().id().clone()
}
WindowMoveDirection::Right => {
@@ -2001,14 +2008,17 @@ impl<W: LayoutElement> ScrollingSpace<W> {
Some(self.options.animations.window_movement.0),
);
let target_position = self
.tile_render_pos(source_col_idx + 1, 0)
.expect("should determine source tile render pos");
let inserted_tile = self.columns[source_col_idx + 1]
.tiles
.first_mut()
.expect("should have first tile");
inserted_tile.animate_move_from(Point::new(
-(inserted_tile.tile_bounding_box().w + self.options.layout.gaps),
extra_offset.y,
));
inserted_tile.animate_move_from(
source_position - target_position + extra_offset,
);
inserted_tile.focused_window().id().clone()
}
@@ -2059,7 +2069,7 @@ impl<W: LayoutElement> ScrollingSpace<W> {
if let Some(ungrouped) = source_tile.ungroup_single(&id) {
let to_update = source_tile.focused_window().id().clone();
let extra_offset = Point::new(0., source_tile.tab_indicator_extra_size().h);
let extra_offset = source_tile.tab_indicator_content_offset();
// we need to refresh the window here to avoid triggering its resize animation
source_tile.focused_window().refresh();
self.update_window(&to_update, None);

View File

@@ -1,24 +1,40 @@
use std::cell::RefCell;
use std::cmp::min;
use std::iter::zip;
use std::mem;
use anyhow::ensure;
use itertools::izip;
use niri_config::{CornerRadius, Gradient, GradientRelativeTo, TabIndicatorPosition};
use smithay::utils::{Logical, Point, Rectangle, Size};
use pango::glib::property::PropertySet;
use pango::FontDescription;
use pangocairo::cairo::{self, ImageSurface};
use smithay::backend::allocator::Fourcc;
use smithay::backend::renderer::element::Kind;
use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture};
use smithay::utils::{Logical, Physical, Point, Rectangle, Size, Transform};
use super::tile::Tile;
use super::LayoutElement;
use crate::animation::{Animation, Clock};
use crate::niri_render_elements;
use crate::render_helpers::border::BorderRenderElement;
use crate::render_helpers::primary_gpu_texture::PrimaryGpuTextureRenderElement;
use crate::render_helpers::renderer::NiriRenderer;
use crate::render_helpers::texture::{TextureBuffer, TextureRenderElement};
use crate::utils::{
floor_logical_in_physical_max1, round_logical_in_physical, round_logical_in_physical_max1,
to_physical_precise_round,
};
const MIN_DIST_TO_EDGES: f64 = 20.;
#[derive(Debug)]
pub struct TabIndicator {
shader_locs: Vec<Point<f64, Logical>>,
shaders: Vec<BorderRenderElement>,
open_anim: Option<Animation>,
tabs: Vec<TabInfo>,
title_textures: Vec<TitleTexture>,
config: niri_config::TabIndicator,
}
@@ -28,19 +44,36 @@ pub struct TabInfo {
pub gradient: Gradient,
/// Tab geometry in the same coordinate system as the area.
pub geometry: Rectangle<f64, Logical>,
/// The title for this tab.
pub title: String,
}
niri_render_elements! {
TabIndicatorRenderElement => {
Gradient = BorderRenderElement,
Title = PrimaryGpuTextureRenderElement,
}
}
#[derive(Debug, Default)]
struct TitleTexture {
title: String,
scale: f64,
max_size: Size<f64, Logical>,
// cached result of the rendered title texture
texture: RefCell<Option<TextureBuffer<GlesTexture>>>,
// the maximum size wanted by the title texture if it had infinite space
wanted_size: RefCell<Option<Size<i32, Physical>>>,
font_size: u32,
}
impl TabIndicator {
pub fn new(config: niri_config::TabIndicator) -> Self {
Self {
shader_locs: Vec::new(),
shaders: Vec::new(),
tabs: Vec::new(),
title_textures: Vec::new(),
open_anim: None,
config,
}
@@ -108,7 +141,7 @@ impl TabIndicator {
(count - 1) as f64 * (px_per_tab + gaps_between) + px_per_tab * progress;
let mut ones_left = ((length - floored_length) / pixel).round() as usize;
let mut shader_loc = Point::from((-gap - width, round((side - length) / 2.)));
let mut shader_loc = Point::from((-width, round((side - length) / 2.)));
match position {
TabIndicatorPosition::Top => mem::swap(&mut shader_loc.x, &mut shader_loc.y),
TabIndicatorPosition::Bottom => {
@@ -141,17 +174,18 @@ impl TabIndicator {
#[allow(clippy::too_many_arguments)]
pub fn update_render_elements(
&mut self,
tabs: Vec<TabInfo>,
enabled: bool,
// Geometry of the tabs area.
area: Rectangle<f64, Logical>,
// View rect relative to the tabs area.
area_view_rect: Rectangle<f64, Logical>,
// Tab count, should match the tabs iterator length.
tab_count: usize,
tabs: impl Iterator<Item = TabInfo>,
is_active: bool,
scale: f64,
) {
self.tabs = tabs;
let tab_count = self.tabs.len();
if !enabled || self.config.off {
self.shader_locs.clear();
self.shaders.clear();
@@ -172,10 +206,40 @@ impl TabIndicator {
let shared_rounded_corners = self.config.gaps_between_tabs == 0.;
let mut tabs_left = tab_count;
let rects = self.tab_rects(area, count, scale);
for ((shader, loc), (tab, rect)) in zip(
zip(&mut self.shaders, &mut self.shader_locs),
zip(tabs, rects),
let rects = self.tab_rects(area, count, scale).collect::<Vec<_>>();
if self.title_textures.len() != self.tabs.len() {
self.title_textures = zip(self.tabs.iter(), rects.iter())
.map(|(t, rect)| {
TitleTexture::new(
t.title.clone(),
scale,
Size::new((rect.size.w - 20.).max(0.), 24.),
self.config.title_font_size,
)
})
.collect();
} else {
izip!(
self.title_textures.iter_mut(),
self.tabs.iter(),
rects.iter(),
)
.for_each(|(tex, t, rect)| {
tex.update_config(
Some(t.title.clone()),
Some(scale),
Some(Size::new((rect.size.w - MIN_DIST_TO_EDGES).max(0.), 16384.)),
Some(self.config.title_font_size),
);
});
}
for (shader, loc, tab, rect) in izip!(
&mut self.shaders,
&mut self.shader_locs,
&self.tabs,
rects.iter(),
) {
*loc = rect.loc;
@@ -257,9 +321,9 @@ impl TabIndicator {
.find_map(|(idx, rect)| rect.contains(point).then_some(idx))
}
pub fn render(
pub fn render<R: NiriRenderer>(
&self,
renderer: &mut impl NiriRenderer,
renderer: &mut R,
pos: Point<f64, Logical>,
) -> impl Iterator<Item = TabIndicatorRenderElement> + '_ {
let has_border_shader = BorderRenderElement::has_shader(renderer);
@@ -267,9 +331,58 @@ impl TabIndicator {
return None.into_iter().flatten();
}
let rv = zip(&self.shaders, &self.shader_locs)
.map(move |(shader, loc)| shader.clone().with_location(pos + *loc))
.map(TabIndicatorRenderElement::from);
let titles = (!self.config.hide_titles).then(|| {
zip(&self.title_textures, &self.shader_locs)
.filter_map(|(tex, loc)| match tex.get(renderer.as_gles_renderer()) {
Ok(texture) => {
let gap_to_bar = 5.;
let pos_x = (tex.max_size.w + MIN_DIST_TO_EDGES) / 2.
- texture.logical_size().w / 2.;
let pos_y = match self.config.position {
TabIndicatorPosition::Top => -gap_to_bar,
TabIndicatorPosition::Bottom => gap_to_bar - texture.logical_size().h,
};
Some(
PrimaryGpuTextureRenderElement(
TextureRenderElement::from_texture_buffer(
texture,
pos + *loc + Point::new(pos_x, pos_y),
1.,
None,
None,
Kind::Unspecified,
),
)
.into(),
)
}
Err(_) => {
// silent fail is ok, we just won't show the title
None
}
})
.collect::<Vec<_>>()
.into_iter()
});
let rv = izip!(&self.shaders, &self.title_textures, &self.shader_locs)
.map(move |(shader, tex, loc)| {
let offset = if !self.config.hide_titles {
match self.config.position {
TabIndicatorPosition::Top => Point::new(0., tex.size().h),
TabIndicatorPosition::Bottom => Point::new(0., -tex.size().h),
}
} else {
Point::default()
};
shader.clone().with_location(pos + *loc + offset)
})
.map(TabIndicatorRenderElement::from)
.chain(titles.into_iter().flatten());
Some(rv).into_iter().flatten()
}
@@ -283,10 +396,26 @@ impl TabIndicator {
let round = |logical: f64| round_logical_in_physical(scale, logical);
let width = round(self.config.width);
let gap = round(self.config.gap);
let font_height = if !self.config.hide_titles {
// we need an initial approximate value here, because when we first spawn the tab
// indicator, the textures are not yet rendered, but the tile resize animation plays
// immediately.
self.title_textures
.iter()
.fold(self.config.title_font_size as f64, |acc, curr| {
if let Some(texture) = curr.texture.borrow().as_ref() {
texture.logical_size().h.max(acc)
} else {
acc
}
})
} else {
0.
};
// No, I am *not* falling into the rabbit hole of "what if the tab indicator is wide enough
// that it peeks from the other side of the window".
let size = f64::max(0., width + gap);
let size = f64::max(0., width + gap + font_height);
Size::from((0., size))
}
@@ -305,14 +434,17 @@ impl TabIndicator {
}
impl TabInfo {
pub fn from_tile<W: LayoutElement>(
tile: &Tile<W>,
position: Point<f64, Logical>,
#[allow(clippy::too_many_arguments)]
pub fn new<W: LayoutElement>(
window: &W,
focus_ring_config: &niri_config::FocusRing,
border_config: &niri_config::FocusRing,
is_active: bool,
is_urgent: bool,
config: &niri_config::TabIndicator,
tile_size: Size<f64, Logical>,
) -> Self {
let rules = tile.focused_window().rules();
let rules = window.rules();
let rule = rules.tab_indicator;
let gradient_from_rule = || {
@@ -342,8 +474,6 @@ impl TabInfo {
let gradient_from_border = || {
// Come up with tab indicator gradient matching the focus ring or the border, whichever
// one is enabled.
let focus_ring_config = tile.focus_ring().config();
let border_config = tile.border().config();
let config = if focus_ring_config.off {
border_config
} else {
@@ -364,8 +494,174 @@ impl TabInfo {
.or_else(gradient_from_config)
.unwrap_or_else(gradient_from_border);
let geometry = Rectangle::new(position, tile.animated_tile_size());
let geometry = Rectangle::new(Point::default(), tile_size);
TabInfo { gradient, geometry }
TabInfo {
gradient,
geometry,
title: window.title().unwrap_or_default(),
}
}
}
impl TitleTexture {
fn new(title: String, scale: f64, max_size: Size<f64, Logical>, font_size: u32) -> Self {
Self {
title,
scale,
texture: Default::default(),
max_size,
wanted_size: Default::default(),
font_size,
}
}
fn update_config(
&mut self,
new_title: Option<String>,
new_scale: Option<f64>,
new_max_size: Option<Size<f64, Logical>>,
new_font_size: Option<u32>,
) {
if let Some(new_font_size) = new_font_size {
if new_font_size != self.font_size {
self.texture.set(None);
self.wanted_size.set(None);
}
self.font_size = new_font_size;
}
if let Some(new_title) = new_title {
if new_title != self.title {
self.texture.set(None);
self.wanted_size.set(None);
}
self.title = new_title;
}
if let Some(new_scale) = new_scale {
if new_scale != self.scale {
self.texture.set(None);
self.wanted_size.set(None);
}
self.scale = new_scale;
}
if let Some(new_max_size) = new_max_size {
if new_max_size != self.max_size {
// if we have a size adjustment, we only need to re-render if either:
// a) the texture's wanted size is larger than the current max size _and_ the new
// max size is larger
// b) the new max size is smaller than the current texture
//
// otherwise the texture will still fully fit within the current allocated space
let should_dirty = if let (Some(texture), Some(wanted_size)) = (
self.texture.borrow().as_ref(),
self.wanted_size.borrow().as_ref(),
) {
let wanted_size = wanted_size.to_f64().to_logical(self.scale);
let tex_size = texture.logical_size();
(wanted_size.w >= self.max_size.w && new_max_size.w > self.max_size.w)
|| (wanted_size.h >= self.max_size.h && new_max_size.h > self.max_size.h)
|| (tex_size.w > new_max_size.w)
|| (tex_size.h > new_max_size.h)
} else {
true
};
if should_dirty {
self.texture.set(None);
self.wanted_size.set(None);
}
}
self.max_size = new_max_size;
}
}
fn get(&self, renderer: &mut GlesRenderer) -> anyhow::Result<TextureBuffer<GlesTexture>> {
let mut tex = self.texture.borrow_mut();
if self.title.is_empty() {
return Err(anyhow::anyhow!(
"cannot render title texture if title is empty"
));
}
match &*tex {
Some(texture) => Ok(texture.clone()),
None => {
let (new_tex, wanted_size) = render_title_texture(
renderer,
&self.title,
self.scale,
self.max_size,
self.font_size,
)?;
*tex = Some(new_tex.clone());
self.wanted_size.set(Some(wanted_size));
Ok(new_tex)
}
}
}
fn size(&self) -> Size<f64, Logical> {
self.texture
.borrow()
.as_ref()
.map(|tex| tex.logical_size())
.unwrap_or_default()
}
}
fn render_title_texture(
renderer: &mut GlesRenderer,
title: &str,
scale: f64,
max_size: Size<f64, Logical>,
font_size: u32,
) -> anyhow::Result<(TextureBuffer<GlesTexture>, Size<i32, Physical>)> {
let _span = tracy_client::span!("tab_indicator::render_title_texture");
// TODO: expose in config
let mut font = FontDescription::from_string(&format!("sans {font_size}px"));
font.set_absolute_size(to_physical_precise_round(scale, font.size()));
let surface = ImageSurface::create(cairo::Format::ARgb32, 0, 0)?;
let cr = cairo::Context::new(&surface)?;
let layout = pangocairo::functions::create_layout(&cr);
layout.context().set_round_glyph_positions(false);
layout.set_single_paragraph_mode(true);
layout.set_font_description(Some(&font));
layout.set_text(title);
let (width, height) = layout.pixel_size();
let wanted_size = Size::new(width, height);
// Guard against overly long window titles.
let max_size = max_size.to_physical_precise_round(scale);
let width = min(width, max_size.w);
let height = min(height, max_size.h);
ensure!(width > 0 && height > 0);
let surface = ImageSurface::create(cairo::Format::ARgb32, width, height)?;
let cr = cairo::Context::new(&surface)?;
cr.set_source_rgb(1., 1., 1.);
pangocairo::functions::show_layout(&cr, &layout);
drop(cr);
let data = surface.take_data().unwrap();
let buffer = TextureBuffer::from_memory(
renderer,
&data,
Fourcc::Argb8888,
(width, height),
false,
scale,
Transform::Normal,
Vec::new(),
)?;
Ok((buffer, wanted_size))
}

View File

@@ -842,7 +842,7 @@ fn appear_group_indicator() {
// when initiating the tab spawn animation, the tile should have a slight offset in the
// beginning
assert_snapshot!(format_tiles(&layout), @"100 × 100 at x: 0 y: -9");
assert_snapshot!(format_tiles(&layout), @"100 × 100 at x: 0 y:-20");
let ops = [Op::AdvanceAnimations { msec_delta: 1000 }];
@@ -901,7 +901,7 @@ fn move_window_into_and_out_of_group_down() {
// y on the ejected window should be slightly positive due to the tab indicator offset
assert_snapshot!(format_tiles(&layout), @r"
100 × 100 at x: 0 y: 0
100 × 100 at x: 0 y: 14
100 × 100 at x: 0 y: 26
");
let ops = [Op::CompleteAnimations];
@@ -913,7 +913,7 @@ fn move_window_into_and_out_of_group_down() {
// grouped tiles have a larger bounding box)
assert_snapshot!(format_tiles(&layout), @r"
100 × 100 at x: 0 y: 0
100 × 100 at x: 0 y:109
100 × 100 at x: 0 y:121
");
let ops = [Op::FocusWindowUp, Op::ToggleGroup, Op::CompleteAnimations];
@@ -976,7 +976,7 @@ fn move_window_into_and_out_of_group_up() {
// y on the ejected window should be slightly positive due to the tab indicator offset
assert_snapshot!(format_tiles(&layout), @r"
100 × 100 at x: 0 y: 5
100 × 100 at x: 0 y: 9
100 × 100 at x: 0 y: 20
");
let ops = [Op::CompleteAnimations];
@@ -1041,7 +1041,7 @@ fn move_window_into_and_out_of_group_right() {
// important: the x difference here should be the same as in the "left" variant of this test.
assert_snapshot!(format_tiles(&layout), @r"
100 × 100 at x: 0 y: 0
100 × 100 at x: 5 y: 9
100 × 100 at x: 5 y: 20
");
let ops = [Op::CompleteAnimations];
@@ -1106,7 +1106,7 @@ fn move_window_into_and_out_of_group_left() {
// important: the x difference here should be the same as in the "right" variant of this test.
assert_snapshot!(format_tiles(&layout), @r"
100 × 100 at x: 41 y: 0
100 × 100 at x: 36 y: 9
100 × 100 at x: 36 y: 20
");
let ops = [Op::CompleteAnimations];

View File

@@ -850,35 +850,36 @@ impl<W: LayoutElement> Tile<W> {
match &self.window {
WindowInner::Single(_) => {
self.tab_indicator.update_render_elements(
vec![],
false,
Rectangle::new(Point::default(), self.tile_bounding_box()),
Rectangle::new(Point::default(), self.animated_bounding_box()),
view_rect,
1,
std::iter::empty(),
is_active,
self.scale,
);
}
WindowInner::Multiple { windows, focus_idx } => {
let tabs = windows
.iter()
.enumerate()
.map(|(idx, w)| {
TabInfo::new(
w,
self.focus_ring().config(),
self.border().config(),
idx == *focus_idx,
w.is_urgent(),
&self.tab_indicator.config(),
self.animated_tile_size(),
)
})
.collect();
self.tab_indicator.update_render_elements(
tabs,
true,
Rectangle::new(Point::default(), self.tile_bounding_box()),
Rectangle::new(Point::default(), self.animated_bounding_box()),
view_rect,
windows.len(),
windows
.iter()
.enumerate()
.map(|(idx, w)| {
TabInfo::from_tile(
self,
Point::default(),
idx == *focus_idx,
w.is_urgent(),
&self.tab_indicator.config(),
)
})
.collect::<Vec<_>>()
.into_iter(),
is_active,
self.scale,
);
@@ -1082,7 +1083,7 @@ impl<W: LayoutElement> Tile<W> {
let out = self.window.ungroup_single(id);
if matches!(&self.window, WindowInner::Single(_)) && extra_size.h > 0. {
self.animate_move_from(Point::new(0., extra_size.h));
self.animate_move_from(self.tab_indicator_content_offset());
}
self.focused_window_mut()
@@ -1096,7 +1097,7 @@ impl<W: LayoutElement> Tile<W> {
self.window.group();
if will_group {
self.animate_move_from(Point::new(0., -self.tab_indicator_extra_size().h));
self.animate_move_from(Point::new(0., 0.) - self.tab_indicator_content_offset());
}
}
@@ -1139,7 +1140,7 @@ impl<W: LayoutElement> Tile<W> {
let extra_size = self.tab_indicator_extra_size();
if extra_size.h > 0. {
self.animate_move_from(Point::new(0., extra_size.h));
self.animate_move_from(self.tab_indicator_content_offset());
}
self.window.ungroup_all(
@@ -1292,11 +1293,11 @@ impl<W: LayoutElement> Tile<W> {
}
pub fn tile_bounding_box(&self) -> Size<f64, Logical> {
let mut size = self.tile_size();
self.tile_size() + self.tab_indicator_extra_size()
}
size += self.tab_indicator_extra_size();
size
pub fn animated_bounding_box(&self) -> Size<f64, Logical> {
self.animated_tile_size() + self.tab_indicator_extra_size()
}
pub fn tile_expected_or_current_size(&self) -> Size<f64, Logical> {
@@ -1953,7 +1954,7 @@ impl<W: LayoutElement> Tile<W> {
match open.render(
renderer,
&elements,
self.animated_tile_size(),
self.animated_bounding_box(),
location,
scale,
tile_alpha,
@@ -2035,7 +2036,7 @@ impl<W: LayoutElement> Tile<W> {
contents: contents.collect(),
blocked_out_contents: blocked_out_contents.collect(),
block_out_from: self.window.focused_window().rules().block_out_from,
size: self.animated_tile_size(),
size: self.animated_bounding_box(),
texture: Default::default(),
blocked_out_texture: Default::default(),
}

View File

@@ -1368,4 +1368,8 @@ impl LayoutElement for Mapped {
}
});
}
fn title(&self) -> Option<String> {
with_toplevel_role(self.toplevel(), |role| role.title.clone())
}
}