atagen

monolith

public
Star0
Watch0

Commit c663ae15e5bc

tarball

atagen <boss@atagen.co>2026-10-19 08:38 UTC

7 files changed+166-0
scale: tell clients the output scale and advertise wp_fractional_scale_v1

Clients rendered every surface for scale 1, which the renderer then stretched. Surfaces now get wl_surface.preferred_buffer_scale and wp_fractional_scale_v1 from the output their window, popup, layer surface or lock surface is on. The first commit of a surface sends it, and anything that moves a window or changes a scale sends it again through apply_layout. X11 windows are left at scale 1 and stretched by the renderer.
Modified file: crates/monolith/src/commit.rs+1 additions
@@ -104,6 +104,7 @@ impl CompositorHandler for State {
104104 let _ = popup.send_configure();
105105 }
106106 self.layer_commit(surface);
107+ self.send_scale_once(surface);
107108 self.dirty.redraw = true;
108109 }
109110 }
Modified file: crates/monolith/src/globals.rs+3 additions
@@ -13,6 +13,7 @@ use smithay::wayland::compositor::CompositorState;
1313 use smithay::wayland::cursor_shape::CursorShapeManagerState;
1414 use smithay::wayland::dmabuf::{DmabufFeedbackBuilder, DmabufGlobal, DmabufState};
1515 use smithay::wayland::foreign_toplevel_list::ForeignToplevelListState;
16+use smithay::wayland::fractional_scale::FractionalScaleManagerState;
1617 use smithay::wayland::idle_inhibit::IdleInhibitManagerState;
1718 use smithay::wayland::image_capture_source::{
1819 ImageCaptureSourceState, OutputCaptureSourceState, ToplevelCaptureSourceState,
@@ -59,6 +60,7 @@ pub struct Protocols {
5960 _output_manager: OutputManagerState,
6061 _cursor_shape: CursorShapeManagerState,
6162 _viewporter: ViewporterState,
63+ _fractional_scale: FractionalScaleManagerState,
6264 }
6365
6466 impl Protocols {
@@ -118,6 +120,7 @@ impl Protocols {
118120 _output_manager: OutputManagerState::new_with_xdg_output::<State>(dh),
119121 _cursor_shape: CursorShapeManagerState::new::<State>(dh),
120122 _viewporter: ViewporterState::new::<State>(dh),
123+ _fractional_scale: FractionalScaleManagerState::new::<State>(dh),
121124 }
122125 }
123126 }
Modified file: crates/monolith/src/lock.rs+14 additions
@@ -39,6 +39,20 @@ impl Lock {
3939 .map(|(_, s)| s)
4040 }
4141
42+ pub fn surfaces(&self) -> impl Iterator<Item = (&Output, &LockSurface)> {
43+ self.surfaces
44+ .iter()
45+ .map(|(o, s)| (o, s))
46+ .filter(|(_, s)| s.alive())
47+ }
48+
49+ pub fn output_of(&self, surface: &WlSurface) -> Option<Output> {
50+ self.surfaces
51+ .iter()
52+ .find(|(_, s)| s.wl_surface() == surface)
53+ .map(|(o, _)| o.clone())
54+ }
55+
4256 /// Drops what belonged to an output that was unplugged.
4357 pub fn forget(&mut self, output: &Output) {
4458 self.surfaces.retain(|(o, _)| o != output);
Modified file: crates/monolith/src/main.rs+1 additions
@@ -21,6 +21,7 @@ mod nested;
2121 mod pacing;
2222 mod popups;
2323 mod power;
24+mod scale;
2425 mod scene;
2526 mod seat;
2627 mod signals;
Added file: crates/monolith/src/scale.rs+144 additions
@@ -0,0 +1,144 @@
1+//! Tells clients how many pixels make a logical one where their surfaces are
2+//! shown, through wl_surface and wp_fractional_scale. X11 windows are left
3+//! alone, since Xwayland draws at scale 1 and the renderer stretches it.
4+
5+use crate::state::State;
6+use smithay::desktop::utils::with_surfaces_surface_tree;
7+use smithay::desktop::{WindowSurfaceType, find_popup_root_surface, layer_map_for_output};
8+use smithay::output::{Output, Scale};
9+use smithay::reexports::wayland_server::Resource;
10+use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
11+use smithay::utils::Transform;
12+use smithay::wayland::compositor::{SurfaceData, send_surface_state, with_states};
13+use smithay::wayland::fractional_scale::{FractionalScaleHandler, with_fractional_scale};
14+
15+/// Set on a surface once it was told a scale, so a commit only pays for the
16+/// first one.
17+struct Told;
18+
19+impl FractionalScaleHandler for State {
20+ fn new_fractional_scale(&mut self, surface: WlSurface) {
21+ self.send_scale(&surface);
22+ }
23+}
24+
25+fn apply(surface: &WlSurface, states: &SurfaceData, scale: Scale) {
26+ send_surface_state(surface, states, scale.integer_scale(), Transform::Normal);
27+ with_fractional_scale(states, |f| f.set_preferred_scale(scale.fractional_scale()));
28+ states.data_map.insert_if_missing(|| Told);
29+}
30+
31+impl State {
32+ /// Tells a surface that has not heard yet, which every commit asks of
33+ /// the surfaces it may have just created.
34+ pub fn send_scale_once(&self, surface: &WlSurface) {
35+ if with_states(surface, |s| s.data_map.get::<Told>().is_none()) {
36+ self.send_scale(surface);
37+ }
38+ }
39+
40+ pub fn send_scale(&self, surface: &WlSurface) {
41+ let xwayland = surface
42+ .client()
43+ .is_some_and(|c| c.get_data::<smithay::xwayland::XWaylandClientData>().is_some());
44+ match self.scale_output(surface).filter(|_| !xwayland) {
45+ Some(output) => {
46+ let scale = output.current_scale();
47+ with_states(surface, |states| apply(surface, states, scale));
48+ }
49+ None => with_states(surface, |states| {
50+ states.data_map.insert_if_missing(|| Told);
51+ }),
52+ }
53+ }
54+
55+ /// Walks every surface the compositor shows, for when a scale or a
56+ /// window's output changed.
57+ pub fn send_scales(&self) {
58+ let layout = &self.wm.layout;
59+ for (id, state) in self.wm.iter() {
60+ if state.window.x11_surface().is_some() {
61+ continue;
62+ }
63+ let output = state
64+ .placed
65+ .map(|(_, _, o)| o)
66+ .or_else(|| layout.window(id)?.output)
67+ .and_then(|o| self.backend.output(o))
68+ .or_else(|| self.pointer_output());
69+ if let Some(scale) = output.map(|o| o.current_scale()) {
70+ state
71+ .window
72+ .with_surfaces(|surface, states| apply(surface, states, scale));
73+ }
74+ }
75+ for output in self.wm.space.outputs() {
76+ let scale = output.current_scale();
77+ for l in layer_map_for_output(output).layers() {
78+ l.with_surfaces(|surface, states| apply(surface, states, scale));
79+ }
80+ }
81+ for (output, surface) in self.services.lock.surfaces() {
82+ let scale = output.current_scale();
83+ with_surfaces_surface_tree(surface.wl_surface(), |surface, states| {
84+ apply(surface, states, scale);
85+ });
86+ }
87+ self.send_pointer_scales();
88+ }
89+
90+ /// The cursor image and the drag icon belong to the pointer's output.
91+ pub fn send_pointer_scales(&self) {
92+ let Some(scale) = self.pointer_output().map(|o| o.current_scale()) else {
93+ return;
94+ };
95+ let cursor = match &self.input.cursor_status {
96+ smithay::input::pointer::CursorImageStatus::Surface(s) => Some(s),
97+ _ => None,
98+ };
99+ let icon = self.input.dnd_icon.as_ref().map(|i| &i.surface);
100+ for surface in cursor.into_iter().chain(icon) {
101+ with_surfaces_surface_tree(surface, |surface, states| apply(surface, states, scale));
102+ }
103+ }
104+
105+ pub fn pointer_output(&self) -> Option<Output> {
106+ self.wm
107+ .space
108+ .output_under(self.input.pointer_pos)
109+ .next()
110+ .or_else(|| self.wm.space.outputs().next())
111+ .cloned()
112+ }
113+
114+ /// The output a surface is mainly shown on.
115+ fn scale_output(&self, surface: &WlSurface) -> Option<Output> {
116+ let mut root = crate::idle::root_surface(surface);
117+ if let Some(popup) = self.wm.popups.find_popup(&root)
118+ && let Ok(parent) = find_popup_root_surface(&popup)
119+ {
120+ root = parent;
121+ }
122+ if let Some(id) = self.window_id(&root) {
123+ if self.wm.window(id)?.x11_surface().is_some() {
124+ return None;
125+ }
126+ let o = self
127+ .wm
128+ .get(id)?
129+ .placed
130+ .map(|(_, _, o)| o)
131+ .or_else(|| self.wm.layout.window(id)?.output)?;
132+ return self.backend.output(o);
133+ }
134+ let on_layer = self.wm.space.outputs().find(|o| {
135+ layer_map_for_output(o)
136+ .layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)
137+ .is_some()
138+ });
139+ on_layer
140+ .cloned()
141+ .or_else(|| self.services.lock.output_of(&root))
142+ .or_else(|| self.pointer_output())
143+ }
144+}
Modified file: crates/monolith/src/seat.rs+2 additions
@@ -27,6 +27,7 @@ impl SeatHandler for State {
2727
2828 fn cursor_image(&mut self, _seat: &Seat<Self>, image: CursorImageStatus) {
2929 self.input.cursor_status = image;
30+ self.send_pointer_scales();
3031 self.dirty.redraw = true;
3132 }
3233
@@ -144,6 +145,7 @@ impl WaylandDndGrabHandler for State {
144145 surface,
145146 offset: Point::default(),
146147 });
148+ self.send_pointer_scales();
147149 self.dirty.redraw = true;
148150 let dh = self.core.display.clone();
149151 match type_ {
Modified file: crates/monolith/src/state.rs+1 additions
@@ -418,6 +418,7 @@ impl State {
418418 self.sync_focus();
419419 self.follow_focus();
420420 self.update_inhibit();
421+ self.send_scales();
421422 self.dirty.redraw = true;
422423 self.dirty.snapshot = true;
423424 }