forked from Mirror/niri
feat(tile/tabs): display tab window titles
This commit is contained in:
12
Cargo.lock
generated
12
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
21
README.md
21
README.md
@@ -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
|
||||
|
||||
BIN
assets/screenshots/groups-blur.png
LFS
Normal file
BIN
assets/screenshots/groups-blur.png
LFS
Normal file
Binary file not shown.
@@ -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>>,
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
|
||||
@@ -1368,4 +1368,8 @@ impl LayoutElement for Mapped {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn title(&self) -> Option<String> {
|
||||
with_toplevel_role(self.toplevel(), |role| role.title.clone())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user