Files
feresaria/compositor/src/render/tab_indicator.rs
T
Samuel Onoja ab11269246 Add workspace screen sharing (#64)
Add workspace selection to the screen-sharing picker with pinned capture, privacy filtering and bounded capture resources. Refresh captured tab labels when title or app-ID metadata changes.
2026-10-25 13:59:34 +01:00

334 lines
11 KiB
Rust

use super::{
AnimatedWindowRenderElement, CropRenderElement, Ferese, GlesRenderer, Logical,
MemoryRenderBufferRenderElement, Output, OverviewChromePart, Physical, Point, Rectangle,
Relocate, RelocateRenderElement, RenderElementKind, RescaleRenderElement, color_with_alpha,
overview_chrome_element, physical_rect,
};
use ferese_layout::{Rect, TabBar, WindowId};
#[allow(clippy::too_many_arguments)]
fn text_element(
state: &mut Ferese,
renderer: &mut GlesRenderer,
output_origin: Point<i32, Logical>,
scale: f64,
rect: Rect,
text: &str,
alpha: f32,
centered: bool,
) -> Option<AnimatedWindowRenderElement> {
let settings = state.tab_indicator_settings;
let color = settings
.text_color
.unwrap_or(state.theme_settings.text_primary_color.0);
let available = (rect.width - 16.0).max(0.0);
let mut title: String = text
.chars()
.filter(|c| !c.is_control())
.take(1024)
.collect();
if state.indicator_text_width(&title, settings.font_size) > available
|| text.chars().count() > 1024
{
let mut boundaries: Vec<_> = title.char_indices().map(|(index, _)| index).collect();
boundaries.push(title.len());
let fits = boundaries.partition_point(|&end| {
state.indicator_text_width(&format!("{}…", &title[..end]), settings.font_size)
<= available
});
if fits == 0 {
title.clear();
} else {
title.truncate(boundaries[fits - 1]);
title.push('…');
}
}
let (buffer, size) = state.indicator_label(&title, scale, color, settings.font_size)?;
let x = if centered {
((rect.width - f64::from(size.w) / scale) * 0.5).max(8.0)
} else {
8.0
};
let location = Point::<i32, Physical>::from((
((rect.x - f64::from(output_origin.x) + x) * scale).round() as i32,
((rect.y - f64::from(output_origin.y) + rect.height * 0.5) * scale).round() as i32
- size.h / 2,
));
let element = MemoryRenderBufferRenderElement::from_buffer(
renderer,
location.to_f64(),
&buffer,
Some(alpha),
Some(Rectangle::from_size(
(f64::from(size.w), f64::from(size.h)).into(),
)),
Some(
(
(f64::from(size.w) / scale).round() as i32,
(f64::from(size.h) / scale).round() as i32,
)
.into(),
),
RenderElementKind::Unspecified,
)
.ok()?;
let element = RescaleRenderElement::from_element(element, location, 1.0);
let element = RelocateRenderElement::from_element(element, (0, 0), Relocate::Relative);
let clip = Rect::new(
rect.x + 8.0,
rect.y,
(rect.width - 16.0).max(0.0),
rect.height,
);
CropRenderElement::from_element(element, scale, physical_rect(clip, output_origin, scale))
.map(Into::into)
}
fn title(state: &Ferese, id: WindowId, capture: bool) -> String {
if capture
&& state
.windows
.window(id)
.is_none_or(|w| state.capture_protected(w))
{
return "Protected window".into();
}
state
.windows
.record(id)
.map(|r| {
if !r.title.trim().is_empty() {
r.title.clone()
} else if !r.app_id.is_empty() {
r.app_id.clone()
} else {
"Window".into()
}
})
.unwrap_or_default()
}
#[allow(clippy::too_many_arguments)]
pub(super) fn elements(
state: &mut Ferese,
renderer: &mut GlesRenderer,
output: &Output,
output_geometry: Rectangle<i32, Logical>,
frame: &crate::state::FrameScene,
bar: &TabBar,
content: Rect,
alpha: f32,
capture: bool,
) -> Vec<AnimatedWindowRenderElement> {
let Some(g) = state.tab_indicator_geometry_for_frame(bar, content, frame) else {
return Vec::new();
};
let scale = output.current_scale().fractional_scale();
let settings = state.tab_indicator_settings;
let anchor = bar.group.windows[0];
let accent = settings
.active_color
.unwrap_or(state.theme_settings.accent_color.0);
let inactive = settings
.inactive_color
.unwrap_or(state.theme_settings.border_color.0);
let mut background = settings
.background_color
.unwrap_or(state.theme_settings.surface_raised_color.0);
let material = crate::effects::resolve_material(
crate::effects::SemanticRole::Popover,
state.theme_settings.material_style,
state.theme_settings.shell_opacity as f32,
);
background[3] = if state.theme_settings.reduce_transparency
|| material.style == crate::config::MaterialStyle::Solid
{
1.0
} else {
background[3] * material.opacity
};
let radius = state.theme_settings.material_radius;
let hover = state
.tab_indicator
.hover
.as_ref()
.filter(|h| h.anchor == anchor)
.map_or(0.0, |h| h.opacity.current.clamp(0.0, 1.0) as f32);
let flash = state
.tab_indicator
.flash
.as_ref()
.filter(|h| h.anchor == anchor)
.map_or(0.0, |h| h.opacity.current.clamp(0.0, 1.0) as f32);
let mut elements = Vec::new();
state.retain_overview_labels();
if hover > 0.0 {
let first = state
.tab_indicator
.first_row
.min(bar.group.windows.len().saturating_sub(g.rows));
for (row, id) in bar
.group
.windows
.iter()
.copied()
.skip(first)
.take(g.rows)
.enumerate()
{
let rect = g.row(row);
let label = title(state, id, capture);
if let Some(element) = text_element(
state,
renderer,
output_geometry.loc,
scale,
rect,
&label,
alpha * hover,
false,
) {
elements.push(element);
}
if id == bar.group.active {
// Match the control center's selected shell buttons: a quiet
// foreground tint, with accent reserved for the active rail.
let mut fill = settings
.text_color
.unwrap_or(state.theme_settings.text_primary_color.0);
fill[3] = 0.14 * alpha * hover;
if let Some(element) = overview_chrome_element(
state,
renderer,
output,
output_geometry,
scale,
(id.0, OverviewChromePart::TabRow),
rect,
fill,
0.0,
(radius - g.padding).clamp(0.0, 14.0).min(rect.height * 0.5),
) {
elements.push(element.into());
}
}
}
if settings.outline_width > 0.0 {
let color = settings
.outline_color
.unwrap_or(state.theme_settings.border_color.0);
if let Some(element) = overview_chrome_element(
state,
renderer,
output,
output_geometry,
scale,
(anchor.0, OverviewChromePart::TabListOutline),
g.list,
color_with_alpha(color, alpha * hover),
settings.outline_width.max(1.0 / scale),
radius.min(g.list.height * 0.5),
) {
elements.push(element.into());
}
}
if let Some(element) = overview_chrome_element(
state,
renderer,
output,
output_geometry,
scale,
(anchor.0, OverviewChromePart::TabList),
g.list,
color_with_alpha(background, alpha * hover),
0.0,
radius.min(g.list.height * 0.5),
) {
elements.push(element.into());
}
} else if flash > 0.0 {
let index = bar
.group
.windows
.iter()
.position(|id| *id == bar.group.active)
.unwrap_or(0);
let label = format!(
"{}/{} · {}",
index + 1,
bar.group.windows.len(),
title(state, bar.group.active, capture)
);
let width = (state.indicator_text_width(&label, settings.font_size) + 24.0)
.min(settings.list_width.min(f64::from(output_geometry.size.w)));
let bounds = Rect::new(
output_geometry.loc.x as f64,
output_geometry.loc.y as f64,
output_geometry.size.w as f64,
output_geometry.size.h as f64,
);
let rect = Rect::new(
(content.x + (content.width - width) * 0.5)
.clamp(bounds.x, bounds.x + bounds.width - width),
(content.y + settings.padding).clamp(bounds.y, bounds.y + bounds.height - g.row_height),
width,
g.row_height,
);
if let Some(element) = text_element(
state,
renderer,
output_geometry.loc,
scale,
rect,
&label,
alpha * flash,
true,
) {
elements.push(element);
}
if let Some(element) = overview_chrome_element(
state,
renderer,
output,
output_geometry,
scale,
(anchor.0, OverviewChromePart::TabFlash),
rect,
color_with_alpha(background, alpha * flash),
0.0,
rect.height * 0.5,
) {
elements.push(element.into());
}
}
for (index, id) in bar.group.windows.iter().copied().enumerate() {
let rect = Rect::new(
g.rail.x,
g.rail.y + index as f64 * g.pitch,
g.rail.width,
g.segment_height,
);
let color = if id == bar.group.active {
accent
} else {
inactive
};
if let Some(element) = overview_chrome_element(
state,
renderer,
output,
output_geometry,
scale,
(id.0, OverviewChromePart::TabRail),
rect,
color_with_alpha(color, alpha),
0.0,
rect.width.min(rect.height) * 0.5,
) {
elements.push(element.into());
}
}
elements
}