use smithay::desktop::Window; #[cfg(test)] use smithay::backend::renderer::element::Id; #[cfg(test)] use smithay::backend::renderer::utils::CommitCounter; use super::{ AnimatedWindowRenderElement, ConstrainAlign, ConstrainBehavior, ConstrainReference, CropRenderElement, CursorImageStatus, CursorImageSurfaceData, Ferese, FrameEffectMetrics, GlesRenderer, Layer, LayerSurface, Logical, MaterialSurface, MemoryRenderBufferRenderElement, Output, Physical, Point, PopupManager, Rectangle, Relocate, RelocateRenderElement, RenderElementKind, Renderer, RescaleRenderElement, Resource, RoundedRect, Size, SolidColorRenderElement, Transform, WaylandSurfaceRenderElement, append_material_surface, backdrop, constrain_space_element, corner_program, layer_map_for_output, material_element, overview_strip_elements, render_elements_from_surface_tree, resize_content_behavior, rounded_visual_rect, rounded_window_elements, window_border_element, window_corner_shape, window_shadow_element, window_tint_element, with_states, }; pub(crate) fn frame_effect_metrics( _elements: &[AnimatedWindowRenderElement], _scale: f64, ) -> FrameEffectMetrics { FrameEffectMetrics } pub(crate) fn sampled_output_elements( state: &mut Ferese, renderer: &mut GlesRenderer, output: &Output, include_cursor: bool, frame: &crate::state::FrameScene, ) -> Vec { scene_elements(state, renderer, output, include_cursor, frame, false, None) } pub(crate) fn capture_output_elements( state: &mut Ferese, renderer: &mut GlesRenderer, output: &Output, include_cursor: bool, frame: &crate::state::FrameScene, ) -> Vec { // Blur textures and resize handoff snapshots must never be shared with // display rendering. Only the capture scene can populate these caches. crate::backends::direct::capture::touch_cache(state); std::mem::swap(&mut state.render, &mut state.capture_render); let elements = scene_elements(state, renderer, output, include_cursor, frame, true, None); std::mem::swap(&mut state.render, &mut state.capture_render); elements } /// The caller supplies resources owned by this capture view. No display or /// other workspace's blur/snapshot cache may populate this scene. pub(crate) fn capture_workspace_elements( state: &mut Ferese, renderer: &mut GlesRenderer, view: &crate::handlers::workspace_capture::WorkspaceSource, include_cursor: bool, frame: &crate::state::FrameScene, resources: &mut super::RenderResources, ) -> Vec { std::mem::swap(&mut state.render, resources); let elements = scene_elements( state, renderer, &view.render_output, include_cursor, frame, true, Some(view), ); std::mem::swap(&mut state.render, resources); elements } fn scene_elements( state: &mut Ferese, renderer: &mut GlesRenderer, output: &Output, include_cursor: bool, frame: &crate::state::FrameScene, capture: bool, workspace: Option<&crate::handlers::workspace_capture::WorkspaceSource>, ) -> Vec { if state.session_lock.active() { let Some(geometry) = state.space.output_geometry(output) else { return Vec::new(); }; let scale = output.current_scale().fractional_scale(); let mut elements = if include_cursor { cursor_elements(state, renderer, geometry, scale) } else { Vec::new() }; let background = state .session_lock .backgrounds .entry(output.clone()) .or_insert_with(|| { smithay::backend::renderer::element::solid::SolidColorBuffer::new( geometry.size, [0.0, 0.0, 0.0, 1.0], ) }); background.resize(geometry.size); let opacity = state.session_lock.idle_opacity; if opacity > 0.0 { let overlay = state .session_lock .idle_overlays .entry(output.clone()) .or_insert_with(|| { smithay::backend::renderer::element::solid::SolidColorBuffer::new( geometry.size, [0.0, 0.0, 0.0, 1.0], ) }); overlay.resize(geometry.size); elements.push( SolidColorRenderElement::from_buffer( overlay, (0, 0), scale, opacity, RenderElementKind::Unspecified, ) .into(), ); } if let Some(surface) = state .session_lock .surfaces .get(output) .filter(|surface| surface.alive()) { elements.extend( render_elements_from_surface_tree::< GlesRenderer, WaylandSurfaceRenderElement, >( renderer, surface.wl_surface(), (0, 0), scale, 1.0, RenderElementKind::Unspecified, ) .into_iter() .map(AnimatedWindowRenderElement::from), ); } elements.push( SolidColorRenderElement::from_buffer( background, (0, 0), scale, 1.0, RenderElementKind::Unspecified, ) .into(), ); return elements; } let Some(output_geometry) = workspace .map(|view| view.geometry) .or_else(|| state.space.output_geometry(output)) else { return Vec::new(); }; let scale = output.current_scale().fractional_scale(); let protected_cursor = capture && state.capture_cursor_protected(); let mut elements = if include_cursor && !protected_cursor { cursor_elements(state, renderer, output_geometry, scale) } else { Vec::new() }; let upper_layers: &[Layer] = if !state.layer_visible_for_frame(output, Layer::Top, frame.overview.is_presenting()) { &[Layer::Overlay] } else { &[Layer::Overlay, Layer::Top] }; if workspace.is_none() { elements.extend(layer_elements(state, renderer, output, upper_layers)); } if frame.overview.is_presenting() { elements.extend(overview_strip_elements( state, renderer, output, output_geometry, scale, frame, capture, )); } let prepare_window = |window: &Window| { // Configure immediately, but present the frame/shadow only after // the first buffer commit has settled the actual client geometry. let ready = if workspace.is_some() { crate::state::window_has_buffer(window) } else { state.window_content_ready(window) }; if (capture && state.capture_protected(window)) || !ready { return None; } let id = *state.windows.ids().get(window)?; // Scrolling columns may sit outside their monitor's rectangle. // They must not reappear on a neighboring output just because // their global animated coordinates overlap it. if workspace.map_or_else( || !state.window_belongs_to_output(id, output), |workspace| state.workspaces.workspace_for_window(id) != Some(workspace.id), ) { return None; } let sample = frame.windows.get(&id)?; let visual = sample.presentation.bounds.current; let presentation_alpha = sample.presentation.alpha(); let decoration_progress = sample.geometry.decorations.clamp(0.0, 1.0); Some(( window.clone(), id, sample, visual, decoration_progress, presentation_alpha, )) }; let windows = if workspace.is_none() && state.overview.is_active() { state .windows .overview_windows() .filter_map(prepare_window) .collect::>() } else if workspace.is_some() { let mut windows = frame .windows .keys() .filter_map(|id| state.windows.window(*id)) .filter_map(prepare_window) .collect::>(); windows.sort_by_key(|(_, id, sample, ..)| { std::cmp::Reverse(state.workspace_capture_stack(*id, sample.geometry)) }); windows } else { state .space .elements() .rev() .filter_map(prepare_window) .collect::>() }; let mut closing = if workspace.is_some() { Default::default() } else { super::closing::grouped_elements( state, renderer, output, windows.iter().map(|(_, id, ..)| *id), frame.delta, ) }; for (window, id, sample, visual, decoration_progress, presentation_alpha) in windows { if let Some(group) = closing.remove(&Some(id)) { elements.extend(group); } elements.extend(super::tabs::tab_elements( state, renderer, output, output_geometry, frame, id, capture, )); let constrain = rounded_visual_rect(visual, output_geometry.loc); let material_surface = window .toplevel() .map(|toplevel| toplevel.wl_surface()) .filter(|surface| { workspace.is_none() && crate::effects::surface_role(surface) .is_some_and(|(role, _)| role == crate::effects::SemanticRole::Modal) }); let window_radius = if material_surface.is_some() { state.theme_settings.material_radius } else { state.theme_settings.window_radius } * decoration_progress; let shape = window_corner_shape(&window); let corners = RoundedRect::new(visual, output_geometry.loc, scale, window_radius).with_shape(shape); let corner_shape_changed = state.render.prepare_window_corners(id, shape); let rounded_clip_program = corner_program(&mut state.render, renderer, shape); // Only overview/close intentionally scale the complete application. let scale_content = sample.presentation.scale_content; let behavior = resize_content_behavior(scale_content); let dim = sample.presentation.dim; if let Some(overlay) = window_tint_element( &mut state.render, renderer, id, constrain, corners, [0.0, 0.0, 0.0, dim as f32 * presentation_alpha], false, output, ) { // Front-to-back: dim the application and its border, not its shadow // or other windows/layer surfaces. The overlay is input-transparent. elements.push(overlay.into()); } if let Some(programs) = rounded_clip_program.clone() { let shadow_offset_y = state.theme_settings.shadow_offset_y; let shadow_blur = state.theme_settings.shadow_blur; let shadow_opacity = state.theme_settings.shadow_opacity; let shadow_color = state.theme_settings.shadow_color.0; let focus = sample.presentation.focus(); let border_width = state.theme_settings.border_width + (state.theme_settings.focus_ring_width - state.theme_settings.border_width) * focus; let border_color = state.theme_settings.border_color.0; let gradient = state.theme_settings.border_gradient; if let Some(border) = window_border_element( &mut state.render, &state.theme_settings, renderer, id, constrain, corners, scale, border_width, border_color, gradient, focus as f32, presentation_alpha * decoration_progress as f32, output, &programs, ) { elements.push(border.into()); } let (offset_factor, blur_factor, opacity_factor) = sample.presentation.shadow_factors(); let shadow = window_shadow_element( &mut state.render, renderer, id, constrain, corners, scale, shadow_offset_y * offset_factor, shadow_blur * blur_factor, shadow_opacity * opacity_factor * f64::from(presentation_alpha) * decoration_progress, shadow_color, output, &programs, ); if let Some(snapshot) = state.render.snapshot(&id) && snapshot.context == renderer.context_id().erased() && (snapshot.scale - scale).abs() < 0.001 && let Some(element) = super::window_content::snapshot_element( snapshot, sample.presentation, corners, scale, output, &programs, crate::presentation::handoff_alpha(snapshot.elapsed), ) { elements.push(element.into()); } elements.extend(rounded_window_elements( renderer, &window, corners, scale, presentation_alpha, sample.geometry.presentation_changed || corner_shape_changed, output, programs.clone(), behavior, sample.presentation.native_size, )); let source = window.geometry().size; if material_surface.is_none() && !scale_content && (source.w < constrain.size.w || source.h < constrain.size.h) { let mut color = state.theme_settings.surface_base_color.0; color[3] = presentation_alpha; if let Some(fill) = window_tint_element( &mut state.render, renderer, id, constrain, corners, color, true, output, ) { // Front-to-back: fill uncovered strips behind the native // content instead of stretching it or exposing wallpaper. elements.push(fill.into()); } } else { state.render.clear_tint(id, true); } if let Some(surface) = material_surface && let Some((background, _)) = material_element( state, renderer, output, surface, MaterialSurface { shape_origin: constrain.loc, geometry: constrain, corners, index: 0, capture_geometry: constrain, alpha: presentation_alpha, }, ) { elements.push(background); } if material_surface.is_none() && let Some(blur) = super::window_blur::window_blur_element( state, renderer, output, sample.presentation, corners, ) { elements.push(blur); } if let Some(shadow) = shadow { elements.push(shadow.into()); } } else { elements.extend(constrain_space_element::< GlesRenderer, _, AnimatedWindowRenderElement, _, _, >( renderer, &window, constrain.loc, presentation_alpha, scale, constrain, ConstrainBehavior { reference: ConstrainReference::Geometry, behavior, align: ConstrainAlign::TOP | ConstrainAlign::LEFT, }, )); } } if let Some(group) = closing.remove(&None) { elements.extend(group); } if workspace.is_none() { elements.extend(layer_elements( state, renderer, output, &[Layer::Bottom, Layer::Background], )); } state.prepare_theme_wallpaper(renderer, workspace.map_or(output, |view| &view.output)); let progress = state.theme_progress(); let wallpaper = state.wallpaper.element(renderer, output); let has_wallpaper = wallpaper.is_some(); if let Some(mut wallpaper) = wallpaper { wallpaper.alpha = progress; elements.push(wallpaper.into()); } if let Some(mut previous) = state.previous_theme_wallpaper(renderer, output) { if !has_wallpaper { previous.alpha = 1. - progress; } elements.push(previous.into()); } backdrop::update(&elements, scale); elements } pub(crate) fn layer_surfaces(output: &Output) -> Vec { layer_map_for_output(output).layers().cloned().collect() } pub(super) fn layer_elements( state: &mut Ferese, renderer: &mut GlesRenderer, output: &Output, requested_layers: &[Layer], ) -> Vec { let scale = output.current_scale().fractional_scale(); let Some(mode) = output.current_mode() else { return Vec::new(); }; let output_crop = layer_output_crop(mode.size, output.current_transform()); let layers = { let map = layer_map_for_output(output); requested_layers .iter() .flat_map(|requested| { map.layers_on(*requested).rev().filter_map(|layer| { map.layer_geometry(layer) .map(|geometry| (geometry, layer.clone())) }) }) .collect::>() }; state.render.surfaces.retain(|surface, _| { surface.is_alive() || state.render.closing.iter().any(|window| { window .material .as_ref() .is_some_and(|(key, ..)| key == surface) }) }); let mut elements = Vec::new(); for (geometry, layer) in layers { for (popup, offset) in PopupManager::popups_for_surface(layer.wl_surface()) { let mut bounds = popup.geometry(); let content_origin = geometry.loc + offset - bounds.loc; bounds.loc = geometry.loc + offset; append_material_surface( state, renderer, output, popup.wl_surface(), bounds, content_origin, scale, output_crop, &mut elements, ); } append_material_surface( state, renderer, output, layer.wl_surface(), geometry, geometry.loc, scale, output_crop, &mut elements, ); } elements } fn layer_output_crop(size: Size, transform: Transform) -> Rectangle { Rectangle::from_size(transform.transform_size(size)) } pub(super) fn cursor_elements( state: &Ferese, renderer: &mut GlesRenderer, output_geometry: Rectangle, scale: f64, ) -> Vec { if state.input_capture.active() { return Vec::new(); } let Some(pointer) = state.seat.get_pointer() else { return Vec::new(); }; let output_crop = Rectangle::::from_size(output_geometry.size).to_physical_precise_round(scale); let origin = Point::::default(); let pointer_location = pointer.current_location() - output_geometry.loc.to_f64(); match &state.cursor_status { CursorImageStatus::Surface(surface) => { let hotspot = with_states(surface, |states| { states .data_map .get::() .map(|attributes| { attributes .lock() .expect( "cursor surface attributes must remain consistent across callbacks", ) .hotspot }) .unwrap_or_default() }); let physical_location = (pointer_location - hotspot.to_f64()).to_physical_precise_round(scale); render_elements_from_surface_tree::< GlesRenderer, WaylandSurfaceRenderElement, >( renderer, surface, physical_location, scale, 1.0, RenderElementKind::Cursor, ) .into_iter() .filter_map(|element| { let element = RescaleRenderElement::from_element(element, origin, 1.0); let element = RelocateRenderElement::from_element(element, origin, Relocate::Relative); CropRenderElement::from_element(element, scale, output_crop).map(Into::into) }) .collect() } CursorImageStatus::Named(_) => { let Some(cursor) = state.named_cursor_frame() else { return Vec::new(); }; let physical_location = cursor.physical_location(pointer_location, scale); let Ok(element) = MemoryRenderBufferRenderElement::from_buffer( renderer, physical_location, &cursor.buffer, None, None, None, RenderElementKind::Cursor, ) else { return Vec::new(); }; let element = RescaleRenderElement::from_element(element, origin, 1.0); let element = RelocateRenderElement::from_element(element, origin, Relocate::Relative); CropRenderElement::from_element(element, scale, output_crop) .map(|element| vec![element.into()]) .unwrap_or_default() } CursorImageStatus::Hidden => Vec::new(), } } #[cfg(test)] mod layer_crop_tests { use super::*; use smithay::backend::renderer::element::Element; #[test] fn layer_crop_keeps_bottom_edge_content_in_every_output_transform() { use smithay::backend::renderer::element::solid::SolidColorRenderElement; let mode = Size::::from((1920, 1080)); for transform in [ Transform::Normal, Transform::_90, Transform::_180, Transform::_270, Transform::Flipped, Transform::Flipped90, Transform::Flipped180, Transform::Flipped270, ] { let size = transform.transform_size(mode); let geometry = Rectangle::new((0, size.h - 36).into(), (size.w, 36).into()); let panel = SolidColorRenderElement::new( Id::new(), geometry, CommitCounter::default(), [1.0; 4], RenderElementKind::Unspecified, ); let crop = CropRenderElement::from_element(panel, 1.0, layer_output_crop(mode, transform)) .expect("bottom panel must remain visible"); assert_eq!(crop.geometry(1.0.into()), geometry, "{transform:?}"); } } }