advance shell integration and session support

This commit is contained in:
Samuel Onoja committed 2026-09-25 09:15:07 +01:00
1 parent 0420242350
commit e85fc1e032
27 files changed
+1165 -201

No files matched your search

+44 -5
View File
@@ -110,6 +110,7 @@ pub struct ThemeSettings {
pub border_color: RgbaColor,
pub accent_color: RgbaColor,
pub shadow_color: RgbaColor,
pub surface_base_color: RgbaColor,
pub window_radius: f64,
pub shadow_offset_y: f64,
pub shadow_blur: f64,
@@ -146,6 +147,8 @@ struct ThemeMaterialConfig {
#[derive(Debug, Deserialize)]
struct ThemeColorsConfig {
#[serde(default = "default_surface_base_color")]
surface_base: String,
#[serde(default = "default_border_color")]
border: String,
#[serde(default = "default_accent_color")]
@@ -157,6 +160,7 @@ struct ThemeColorsConfig {
impl Default for ThemeColorsConfig {
fn default() -> Self {
Self {
surface_base: default_surface_base_color(),
border: default_border_color(),
accent: default_accent_color(),
shadow: default_shadow_color(),
@@ -235,12 +239,14 @@ struct BindingModifiers {
pub enum BindingAction {
Spawn(Vec<String>),
Close,
Exit,
Focus(ferese_layout::Direction),
Move(ferese_layout::Direction),
Resize(ferese_layout::Direction),
SwitchWorkspace(u8),
MoveToWorkspace(u8),
ToggleFullscreen,
ToggleMaximized,
ToggleLayout,
CycleColumnWidth,
CenterColumn,
@@ -803,6 +809,10 @@ impl Config {
border_color: parse_color(&self.theme.colors.border, "colors.border")?,
accent_color: parse_color(&self.theme.colors.accent, "colors.accent")?,
shadow_color: parse_color(&self.theme.colors.shadow, "colors.shadow")?,
surface_base_color: parse_color(
&self.theme.colors.surface_base,
"colors.surface_base",
)?,
window_radius,
shadow_offset_y,
shadow_blur,
@@ -1059,6 +1069,10 @@ fn parse_action(
no_argument()?;
Ok(BindingAction::Close)
}
"exit" => {
no_argument()?;
Ok(BindingAction::Exit)
}
"focus" => Ok(BindingAction::Focus(parse_direction(required_argument()?)?)),
"move" => Ok(BindingAction::Move(parse_direction(required_argument()?)?)),
"resize" => Ok(BindingAction::Resize(
@@ -1070,6 +1084,10 @@ fn parse_action(
"move-to-workspace" => Ok(BindingAction::MoveToWorkspace(parse_workspace(
required_argument()?,
)?)),
"toggle-maximized" => {
no_argument()?;
Ok(BindingAction::ToggleMaximized)
}
"toggle-fullscreen" => {
no_argument()?;
Ok(BindingAction::ToggleFullscreen)
@@ -1133,7 +1151,8 @@ fn default_bindings() -> Vec<BindingConfig> {
let mut bindings = vec![
binding("Super+Enter", "spawn", Some("terminal")),
binding("Super+Q", "close", None),
binding("Super+F", "toggle-fullscreen", None),
binding("Super+F", "toggle-maximized", None),
binding("Super+Shift+F", "toggle-fullscreen", None),
binding("Super+M", "toggle-layout", None),
binding("Super+R", "cycle-column-width", None),
binding("Super+C", "center-column", None),
@@ -1141,6 +1160,7 @@ fn default_bindings() -> Vec<BindingConfig> {
binding("Super+]", "expel", None),
binding("Super+Shift+Space", "toggle-floating", None),
binding("Super+Tab", "toggle-overview", None),
binding("Super+Shift+E", "exit", None),
];
for (key, direction) in [("H", "left"), ("J", "down"), ("K", "up"), ("L", "right")] {
@@ -1283,13 +1303,17 @@ const fn default_inner_gap() -> f64 {
}
const fn default_outer_gap() -> f64 {
10.0
4.0
}
fn default_border_color() -> String {
"#FFFFFF18".to_owned()
}
fn default_surface_base_color() -> String {
"#111821".to_owned()
}
fn default_accent_color() -> String {
"#5B8CFF".to_owned()
}
@@ -1574,6 +1598,7 @@ mod tests {
border_color: RgbaColor([17.0 / 255.0, 34.0 / 255.0, 51.0 / 255.0, 68.0 / 255.0,]),
accent_color: RgbaColor([170.0 / 255.0, 187.0 / 255.0, 0.8, 1.0]),
shadow_color: RgbaColor([1.0 / 255.0, 2.0 / 255.0, 3.0 / 255.0, 128.0 / 255.0]),
surface_base_color: RgbaColor([17.0 / 255.0, 24.0 / 255.0, 33.0 / 255.0, 1.0,]),
window_radius: 12.0,
shadow_offset_y: -2.0,
shadow_blur: 24.0,
@@ -1628,7 +1653,21 @@ mod tests {
let input = config.input_settings().unwrap();
let bindings = config.bindings(&input).unwrap();
assert_eq!(bindings.len(), 40);
assert_eq!(bindings.len(), 42);
for (shift, action) in [
(false, BindingAction::ToggleMaximized),
(true, BindingAction::ToggleFullscreen),
] {
assert!(bindings.iter().any(|binding| binding.modifiers.logo
&& binding.modifiers.shift == shift
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_f)
&& binding.action == action));
}
assert!(
bindings
.iter()
.any(|binding| binding.action == BindingAction::Exit)
);
assert!(bindings.iter().any(|binding| {
binding.modifiers.logo
&& binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Return)
@@ -1650,7 +1689,7 @@ mod tests {
let input = replaced.input_settings().unwrap();
let bindings = replaced.bindings(&input).unwrap();
assert_eq!(bindings.len(), 40);
assert_eq!(bindings.len(), 42);
assert!(bindings.iter().any(|binding| {
binding.action
== BindingAction::Spawn(vec![
@@ -1662,7 +1701,7 @@ mod tests {
let input = unbound.input_settings().unwrap();
let bindings = unbound.bindings(&input).unwrap();
assert_eq!(bindings.len(), 39);
assert_eq!(bindings.len(), 41);
assert!(
!bindings
.iter()
+22
View File
@@ -198,6 +198,28 @@ impl XdgShellHandler for Ferese {
);
}
fn maximize_request(&mut self, surface: ToplevelSurface) {
if let Some(id) = self.window_ids.iter().find_map(|(window, id)| {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
.then_some(*id)
}) {
self.set_window_maximized(id, true);
}
}
fn unmaximize_request(&mut self, surface: ToplevelSurface) {
if let Some(id) = self.window_ids.iter().find_map(|(window, id)| {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == surface.wl_surface())
.then_some(*id)
}) {
self.set_window_maximized(id, false);
}
}
fn fullscreen_request(
&mut self,
surface: ToplevelSurface,
+45 -6
View File
@@ -44,6 +44,14 @@ impl Ferese {
}
let symbol = keysym.modified_sym().raw();
if overview_escape(symbol, data.overview.is_active()) {
if state == KeyState::Pressed {
data.intercepted_keys.insert(keycode);
data.set_overview_active(false);
}
return FilterResult::Intercept(());
}
if emergency_shortcut_escape(symbol, modifiers.ctrl, modifiers.alt) {
if state == KeyState::Pressed {
data.intercepted_keys.insert(keycode);
@@ -168,11 +176,29 @@ impl Ferese {
let serial = SERIAL_COUNTER.next_serial();
if self.overview.is_active() {
if event.state() == ButtonState::Pressed
&& let Some(window) = self.window_under_visual(pointer.current_location())
&& let Some(id) = self.window_ids.get(&window).copied()
{
self.select_overview_window(id);
let position = pointer.current_location();
if self.layer_under(position).is_some() {
pointer.button(
self,
&ButtonEvent {
button: event.button_code(),
state: event.state(),
serial,
time: event.time() as u32,
},
);
pointer.frame(self);
return;
}
if event.state() == ButtonState::Pressed {
if let Some(window) = self.window_under_visual(position)
&& let Some(id) = self.window_ids.get(&window).copied()
{
self.select_overview_window(id);
} else {
self.set_overview_active(false);
}
}
return;
@@ -465,6 +491,7 @@ impl Ferese {
}
}
BindingAction::Close => self.close_focused_window(),
BindingAction::Exit => self.loop_signal.stop(),
BindingAction::Focus(direction) => self.focus_direction(direction),
BindingAction::Move(direction) => self.move_direction(direction),
BindingAction::Resize(direction) => self.resize_direction(direction),
@@ -475,6 +502,7 @@ impl Ferese {
self.move_focused_to_workspace(u32::from(workspace));
}
BindingAction::ToggleFullscreen => self.toggle_focused_fullscreen(),
BindingAction::ToggleMaximized => self.toggle_focused_maximized(),
BindingAction::ToggleLayout => self.toggle_layout_mode(),
BindingAction::CycleColumnWidth => self.cycle_focused_column_width(),
BindingAction::CenterColumn => self.center_focused_column(),
@@ -503,9 +531,20 @@ fn emergency_shortcut_escape(symbol: u32, ctrl: bool, alt: bool) -> bool {
ctrl && alt && symbol == keysyms::KEY_Escape
}
fn overview_escape(symbol: u32, overview_active: bool) -> bool {
overview_active && symbol == keysyms::KEY_Escape
}
#[cfg(test)]
mod tests {
use super::{emergency_shortcut_escape, virtual_terminal};
use super::{emergency_shortcut_escape, overview_escape, virtual_terminal};
#[test]
fn escape_closes_only_an_active_overview() {
assert!(overview_escape(keysyms::KEY_Escape, true));
assert!(!overview_escape(keysyms::KEY_Escape, false));
assert!(!overview_escape(keysyms::KEY_q, true));
}
#[test]
fn emergency_escape_requires_control_alt_escape() {
+29 -7
View File
@@ -16,7 +16,7 @@ use ferese_core::LayoutMode;
use ferese_ipc::{Request, Response, VERSION, read_frame, write_frame};
use ferese_layout::Direction;
use serde_json::{Value, json};
use smithay::reexports::calloop::{EventLoop, channel};
use smithay::reexports::calloop::{EventLoop, LoopSignal, channel};
use smithay::utils::Transform;
use crate::{Ferese, config::OutputTransform};
@@ -84,15 +84,20 @@ pub(crate) fn init(
}
})?;
let signal = event_loop.get_signal();
thread::Builder::new()
.name("ferese-ipc-listener".to_owned())
.spawn(move || accept_connections(listener, sender))?;
.spawn(move || accept_connections(listener, sender, signal))?;
tracing::info!(path = %path.display(), "Ferese IPC is accepting connections");
Ok(guard)
}
fn accept_connections(listener: UnixListener, sender: channel::SyncSender<IpcCall>) {
fn accept_connections(
listener: UnixListener,
sender: channel::SyncSender<IpcCall>,
signal: LoopSignal,
) {
let active_connections = Arc::new(AtomicUsize::new(0));
loop {
@@ -125,11 +130,12 @@ fn accept_connections(listener: UnixListener, sender: channel::SyncSender<IpcCal
continue;
};
let sender = sender.clone();
let signal = signal.clone();
if let Err(error) = thread::Builder::new()
.name("ferese-ipc-client".to_owned())
.spawn(move || {
let _permit = permit;
serve_connection(stream, sender);
serve_connection(stream, sender, signal);
})
{
tracing::warn!(%error, "could not start IPC connection worker");
@@ -154,9 +160,13 @@ fn try_acquire_connection(active: &Arc<AtomicUsize>) -> Option<ConnectionPermit>
})
}
fn serve_connection(mut stream: UnixStream, sender: channel::SyncSender<IpcCall>) {
fn serve_connection(
mut stream: UnixStream,
sender: channel::SyncSender<IpcCall>,
signal: LoopSignal,
) {
loop {
let request = match read_frame(&mut stream) {
let request: Request = match read_frame(&mut stream) {
Ok(request) => request,
Err(ferese_ipc::FrameError::Io(error))
if matches!(
@@ -174,6 +184,7 @@ fn serve_connection(mut stream: UnixStream, sender: channel::SyncSender<IpcCall>
}
};
let (response, receiver) = sync_channel(1);
let exit_requested = request.command == "exit";
let call = IpcCall { request, response };
if sender.try_send(call).is_err() {
tracing::warn!("disconnecting IPC client because the request queue is full");
@@ -182,10 +193,17 @@ fn serve_connection(mut stream: UnixStream, sender: channel::SyncSender<IpcCall>
let Ok(response) = receiver.recv() else {
return;
};
let exit_accepted = exit_requested && response.error.is_none();
if let Err(error) = write_frame(&mut stream, &response) {
tracing::debug!(%error, "IPC client disconnected before receiving its response");
return;
}
if exit_accepted {
// Acknowledge before stopping so feresectl never races process exit.
signal.stop();
signal.wakeup();
return;
}
}
}
@@ -203,6 +221,7 @@ impl Ferese {
fn dispatch_ipc_command(&mut self, command: &str, args: &Value) -> Result<Value, CommandError> {
match command {
"exit" => {} // The IPC worker stops the loop after writing the response.
"focus" => self.focus_direction(direction_arg(args)?),
"move" => self.move_direction(direction_arg(args)?),
"resize" => self.resize_direction(direction_arg(args)?),
@@ -210,6 +229,7 @@ impl Ferese {
"move-to-workspace" => self.move_focused_to_workspace(workspace_arg(args)?),
"toggle-floating" => self.toggle_focused_floating(),
"toggle-fullscreen" => self.toggle_focused_fullscreen(),
"toggle-maximized" => self.toggle_focused_maximized(),
"toggle-layout" => self.toggle_layout_mode(),
"cycle-column-width" => self.cycle_focused_column_width(),
"center-column" => self.center_focused_column(),
@@ -608,7 +628,9 @@ mod tests {
fn malformed_ipc_connection_is_closed_without_reaching_the_request_queue() {
let (mut client, server) = UnixStream::pair().unwrap();
let (sender, _receiver) = channel::sync_channel(1);
let worker = thread::spawn(move || serve_connection(server, sender));
let event_loop = EventLoop::<()>::try_new().unwrap();
let signal = event_loop.get_signal();
let worker = thread::spawn(move || serve_connection(server, sender, signal));
let invalid = b"not-json";
client
+93 -6
View File
@@ -6,7 +6,7 @@ use std::{
time::{Duration, Instant},
};
use ferese_animation::{AnimatedValue, ClientSize, SpringConfig, WindowGeometry};
use ferese_animation::{AnimatedValue, ClientSize, PresentationMode, SpringConfig, WindowGeometry};
use ferese_core::{
LayoutMode, OutputGeometry, OutputId, OutputWorkspaceMap, WindowPlacement, WorkspaceId,
WorkspaceSet,
@@ -118,6 +118,7 @@ pub struct Ferese {
output_identity_ids: HashMap<String, OutputId>,
pub window_ids: HashMap<Window, WindowId>,
pub window_geometry: HashMap<WindowId, WindowGeometry>,
maximized_windows: HashSet<WindowId>,
window_stack: crate::stacking::WindowStack,
pub(crate) window_borders: HashMap<WindowId, crate::winit::WindowBorderBuffers>,
pub(crate) window_shadows: HashMap<WindowId, crate::winit::WindowShadowBuffers>,
@@ -446,6 +447,7 @@ impl Ferese {
output_identity_ids: HashMap::new(),
window_ids: HashMap::new(),
window_geometry: HashMap::new(),
maximized_windows: HashSet::new(),
window_stack: crate::stacking::WindowStack::default(),
window_borders: HashMap::new(),
window_shadows: HashMap::new(),
@@ -619,6 +621,9 @@ impl Ferese {
let map = layer_map_for_output(output);
for requested in layers {
if *requested == Layer::Top && self.output_has_fullscreen(output) {
continue;
}
for layer in map.layers_on(*requested).rev() {
let geometry = map.layer_geometry(layer)?;
let layer_position = output_position - geometry.loc.to_f64();
@@ -901,6 +906,7 @@ impl Ferese {
self.space.unmap_elem(window);
self.window_geometry.remove(&id);
self.maximized_windows.remove(&id);
self.window_stack.remove(id);
self.window_borders.remove(&id);
self.window_shadows.remove(&id);
@@ -996,8 +1002,12 @@ impl Ferese {
continue;
}
let is_maximized =
self.maximized_windows.contains(id) && workspace_fullscreen != Some(*id);
let rect = if workspace_fullscreen == Some(*id) {
fullscreen_bounds
} else if is_maximized {
maximized_rect(bounds, self.gap_config.outer)
} else {
match self.workspaces.placement(*id) {
Some(WindowPlacement::Tiled) => {
@@ -1017,7 +1027,7 @@ impl Ferese {
);
visible.insert(*id);
let scrolling = if !is_fullscreen && !is_floating {
let scrolling = if !is_fullscreen && !is_maximized && !is_floating {
viewport_target
.zip(viewport_current)
.map(|(target, current)| (workspace_id, rect.x + target, current))
@@ -1030,6 +1040,7 @@ impl Ferese {
*id,
rect,
is_fullscreen,
is_maximized,
is_floating,
scrolling,
pending_column_width_cycles.contains(id) && viewport_target_changed,
@@ -1053,7 +1064,9 @@ impl Ferese {
let mut scrolling_world_x = HashMap::new();
placements.sort_by_key(|(_, id, ..)| self.window_stack.rank(*id));
for (window, id, rect, is_fullscreen, is_floating, scrolling, couple_width) in placements {
for (window, id, rect, is_fullscreen, is_maximized, is_floating, scrolling, couple_width) in
placements
{
// A viewport-coupled width must never override fullscreen/floating geometry.
if scrolling.is_none() {
self.viewport_coupled_widths.remove(&id);
@@ -1064,7 +1077,14 @@ impl Ferese {
.window_geometry
.entry(id)
.or_insert_with(|| WindowGeometry::new(rect, committed_size));
let mut requested_size = geometry.set_presentation_target(rect, is_fullscreen, now);
let mode = if is_fullscreen {
PresentationMode::Fullscreen
} else if is_maximized {
PresentationMode::Maximized
} else {
PresentationMode::Normal
};
let mut requested_size = geometry.set_presentation_mode(rect, mode, now);
if !self.animations_enabled {
geometry.advance(Duration::ZERO, self.spring_config, false);
requested_size = geometry.presentation_size_request(now).or(requested_size);
@@ -1132,6 +1152,11 @@ impl Ferese {
state.states.unset(xdg_toplevel::State::Fullscreen)
};
let maximized_changed = if is_maximized {
state.states.set(xdg_toplevel::State::Maximized)
} else {
state.states.unset(xdg_toplevel::State::Maximized)
};
let tiled = !is_floating && !is_fullscreen;
let tiled_changed = if tiled {
state.states.set(xdg_toplevel::State::TiledLeft)
@@ -1145,7 +1170,7 @@ impl Ferese {
| state.states.unset(xdg_toplevel::State::TiledBottom)
};
let decoration_mode = if is_floating && !is_fullscreen {
let decoration_mode = if is_floating && !is_fullscreen && !is_maximized {
DecorationMode::ClientSide
} else {
DecorationMode::ServerSide
@@ -1153,7 +1178,7 @@ impl Ferese {
let decoration_changed = state.decoration_mode != Some(decoration_mode);
state.decoration_mode = Some(decoration_mode);
fullscreen_changed || tiled_changed || decoration_changed
fullscreen_changed || maximized_changed || tiled_changed || decoration_changed
});
if requested_size.is_some() || state_changed {
@@ -1600,6 +1625,42 @@ impl Ferese {
self.relayout();
}
pub fn toggle_focused_maximized(&mut self) {
let Some(window) = self.focused_window else {
return;
};
// Super+F from true fullscreen enters decorated maximization.
let fullscreen = self
.workspaces
.workspace_for_window(window)
.and_then(|workspace| self.workspaces.workspace(workspace))
.is_some_and(|workspace| workspace.fullscreen == Some(window));
let enabled = fullscreen || !self.maximized_windows.contains(&window);
self.set_window_maximized(window, enabled);
}
pub fn set_window_maximized(&mut self, window: WindowId, enabled: bool) {
if self.workspaces.workspace_for_window(window).is_none() {
return;
}
if enabled {
let _ = self.workspaces.set_fullscreen(window, false);
self.maximized_windows.insert(window);
} else {
self.maximized_windows.remove(&window);
}
self.relayout();
}
pub(crate) fn output_has_fullscreen(&self, output: &Output) -> bool {
!self.overview.is_presenting()
&& self
.output_id(output)
.and_then(|id| self.output_workspaces.active_workspace(id))
.and_then(|workspace| self.workspaces.workspace(workspace))
.is_some_and(|workspace| workspace.fullscreen.is_some())
}
pub fn close_focused_window(&mut self) {
let Some(focused) = self.focused_window else {
return;
@@ -1950,6 +2011,18 @@ impl Ferese {
}
}
fn maximized_rect(bounds: Rect, gap: f64) -> Rect {
let gap = gap
.max(0.0)
.min(((bounds.width.min(bounds.height) - 1.0) / 2.0).max(0.0));
Rect::new(
bounds.x + gap,
bounds.y + gap,
(bounds.width - 2.0 * gap).max(1.0),
(bounds.height - 2.0 * gap).max(1.0),
)
}
fn centered_floating_rect(bounds: Rect) -> Rect {
let width = (bounds.width * 0.6).max(1.0);
let height = (bounds.height * 0.6).max(1.0);
@@ -2017,6 +2090,20 @@ impl ClientData for ClientState {
mod tests {
use super::*;
#[test]
fn maximization_respects_layer_exclusion_and_outer_gaps() {
assert_eq!(
maximized_rect(Rect::new(0.0, 48.0, 1280.0, 752.0), 10.0),
Rect::new(10.0, 58.0, 1260.0, 732.0)
);
assert_eq!(
maximized_rect(Rect::new(1600.0, 36.0, 1280.0, 764.0), 6.0),
Rect::new(1606.0, 42.0, 1268.0, 752.0)
);
let tiny = maximized_rect(Rect::new(0.0, 0.0, 5.0, 3.0), 10.0);
assert!(tiny.width >= 1.0 && tiny.height >= 1.0);
}
#[test]
fn transient_geometry_is_centered_and_bounded_by_its_parent() {
let parent = Rect::new(100.0, 50.0, 1_000.0, 800.0);
+29 -62
View File
@@ -40,9 +40,9 @@ use smithay::{
},
input::pointer::{CursorImageStatus, CursorImageSurfaceData},
output::{Mode, Output, PhysicalProperties, Scale, Subpixel},
reexports::calloop::EventLoop,
reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind as PresentationKind,
reexports::wayland_server::Resource,
reexports::calloop::EventLoop,
utils::{
Buffer, Clock, Logical, Monotonic, Physical, Point, Rectangle, Scale as RenderScale, Size,
Transform,
@@ -871,12 +871,12 @@ fn output_elements(
} else {
Vec::new()
};
elements.extend(layer_elements(
state,
renderer,
output,
&[Layer::Overlay, Layer::Top],
));
let upper_layers: &[Layer] = if state.output_has_fullscreen(output) {
&[Layer::Overlay]
} else {
&[Layer::Overlay, Layer::Top]
};
elements.extend(layer_elements(state, renderer, output, upper_layers));
let windows = state
.space
.elements()
@@ -1452,30 +1452,26 @@ fn layer_elements(
let mut elements = Vec::new();
for (geometry, layer) in layers {
let target = geometry.to_physical_precise_round(scale);
let surface_elements = AsRenderElements::<GlesRenderer>::render_elements::<
WaylandSurfaceRenderElement<GlesRenderer>,
>(&layer, renderer, target.loc, scale.into(), 1.0);
let source_bounds = surface_elements
.iter()
.map(|element| element.geometry(scale.into()))
.reduce(Rectangle::merge);
let content_scale = source_bounds
.map(|source| layer_content_scale(source.size, target.size))
.unwrap_or_else(|| RenderScale::from(1.0));
let scale_origin = source_bounds.map_or(target.loc, |source| source.loc);
let crop = target.intersection(output_crop).unwrap_or_default();
elements.extend(
AsRenderElements::<GlesRenderer>::render_elements::<
WaylandSurfaceRenderElement<GlesRenderer>,
>(
&layer,
renderer,
geometry.loc.to_physical_precise_round(scale),
scale.into(),
1.0,
)
.into_iter()
.filter_map(|element| {
let origin = Point::<i32, Physical>::default();
let element = RescaleRenderElement::from_element(element, origin, 1.0);
let element =
RelocateRenderElement::from_element(element, origin, Relocate::Relative);
elements.extend(surface_elements.into_iter().filter_map(|element| {
let element = RescaleRenderElement::from_element(element, scale_origin, content_scale);
let element = RelocateRenderElement::from_element(
element,
Point::<i32, Physical>::default(),
Relocate::Relative,
);
CropRenderElement::from_element(element, scale, crop).map(Into::into)
}));
CropRenderElement::from_element(element, scale, output_crop).map(Into::into)
}),
);
if let Some(material) = material_element(state, renderer, output, &layer, geometry) {
elements.push(material);
@@ -1485,20 +1481,6 @@ fn layer_elements(
elements
}
fn layer_content_scale(
source: Size<i32, Physical>,
target: Size<i32, Physical>,
) -> RenderScale<f64> {
if source.w <= 0 || source.h <= 0 || target.w <= 0 || target.h <= 0 {
return RenderScale::from(1.0);
}
RenderScale::from((
f64::from(target.w) / f64::from(source.w),
f64::from(target.h) / f64::from(source.h),
))
}
fn material_element(
state: &mut Ferese,
renderer: &mut GlesRenderer,
@@ -1519,7 +1501,8 @@ fn material_element(
expanded_blur_region(geometry, material.blur.ceil() as i32, output_logical);
let sample_physical = sample_geometry.to_physical_precise_round(scale);
let transform = output.current_transform().invert();
let tint = [0.067, 0.094, 0.129, material.opacity];
let [red, green, blue, _] = state.theme_settings.surface_base_color.0;
let tint = [red, green, blue, material.opacity];
let parameters = MaterialParameters {
geometry,
tint,
@@ -1831,8 +1814,7 @@ mod tests {
use super::{
color_with_alpha, expanded_blur_region, framebuffer_capture_rect, framebuffer_clip_rect,
layer_content_scale, normalized_scale, rounded_visual_rect, scaled_visual_rect,
shadow_bounds,
normalized_scale, rounded_visual_rect, scaled_visual_rect, shadow_bounds,
};
#[derive(Debug)]
@@ -1885,21 +1867,6 @@ mod tests {
assert_eq!(normalized_scale(f64::INFINITY), 1.0);
}
#[test]
fn layer_content_tracks_new_allocation_before_the_client_repaints() {
let scale = layer_content_scale((1_000, 40).into(), (1_600, 40).into());
assert_eq!(scale, Scale::from((1.6, 1.0)));
}
#[test]
fn layer_content_scale_ignores_empty_allocations() {
assert_eq!(
layer_content_scale((0, 40).into(), (1_600, 40).into()),
Scale::from(1.0)
);
}
#[test]
fn clip_rect_matches_smithay_gles_projection() {
let geometry = Rectangle::<i32, Physical>::new((10, 5).into(), (30, 40).into());