atagen

monolith

public
Star0
Watch0

Commit 599f8f3cc5f6

tarball

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

2 files changed+71-8
drm: take the internal panel out of the layout while the lid is shut

A closed lid only darkened the eDP, which stayed in the layout and Space, so the pointer could enter it, windows could map onto it and mouse-follows-focus could warp there. set_parked now removes it through Layout::remove_output and Space::unmap_output while keeping its DrmOutput, and add_output on open brings its windows back.
Modified file: crates/monolith/src/drm.rs+60 additions−4 deletions
@@ -21,8 +21,8 @@ use smithay::backend::drm::{
2121 DrmDevice, DrmDeviceFd, DrmEvent, DrmEventMetadata, DrmEventTime, DrmNode, NodeType,
2222 };
2323 use smithay::backend::input::{
24- AbsolutePositionEvent, Axis, AxisSource, Event, InputEvent, KeyboardKeyEvent, PointerAxisEvent,
25- PointerButtonEvent, PointerMotionEvent,
24+ AbsolutePositionEvent, Axis, AxisSource, Event, InputEvent, InputTime, KeyboardKeyEvent,
25+ PointerAxisEvent, PointerButtonEvent, PointerMotionEvent,
2626 };
2727 use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface};
2828 use smithay::backend::session::libseat::LibSeatSession;
@@ -94,6 +94,8 @@ struct Surface {
9494 queued_at: u64,
9595 /// The panel is lit. Off outputs are not rendered to.
9696 powered: bool,
97+ /// Out of the layout and Space, but still driven so it can relight.
98+ parked: bool,
9799 /// Fires when a queued frame's vblank never arrives.
98100 watchdog: Option<RegistrationToken>,
99101 /// Consecutive frames that failed to render or queue.
@@ -466,6 +468,7 @@ fn connect(state: &mut State, connector: connector::Info, crtc: crtc::Handle) {
466468 queued: false,
467469 queued_at: 0,
468470 powered: true,
471+ parked: false,
469472 watchdog: None,
470473 failures: 0,
471474 redraw: true,
@@ -636,8 +639,14 @@ pub fn reconfigure_outputs(state: &mut State) {
636639 else {
637640 return;
638641 };
639- let mut surfaces: Vec<&mut Surface> = drm.surfaces.values_mut().collect();
640- surfaces.sort_by_key(|s| s.output.current_location().x);
642+ let mut surfaces: Vec<&mut Surface> = drm.surfaces.values_mut().filter(|s| !s.parked).collect();
643+ surfaces.sort_by_cached_key(|s| {
644+ let name = s.output.name();
645+ (
646+ s.output.current_location().x,
647+ !crate::power::internal(&name),
648+ )
649+ });
641650 let mut right = 0;
642651 let mut changed = false;
643652 for s in surfaces {
@@ -727,7 +736,54 @@ fn disconnect(state: &mut State, crtc: crtc::Handle) {
727736 state.protocols.output_power.output_removed(&surface.output);
728737 }
729738
739+/// Takes the output called `name` out of the layout and Space, or puts it
740+/// back. Its windows move to the other outputs and return when it does.
741+pub fn set_parked(state: &mut State, name: &str, parked: bool) {
742+ let State {
743+ backend: Backend::Drm(drm),
744+ wm: Wm { layout, space, .. },
745+ ..
746+ } = state
747+ else {
748+ return;
749+ };
750+ let Some(s) = drm.surfaces.values_mut().find(|s| s.output.name() == name) else {
751+ return;
752+ };
753+ if s.parked == parked {
754+ return;
755+ }
756+ s.parked = parked;
757+ tracing::info!(name, parked, "output parked");
758+ if parked {
759+ space.unmap_output(&s.output);
760+ layout.remove_output(s.id);
761+ } else if let Some(mode) = s.output.current_mode() {
762+ let position = s.output.current_location();
763+ space.map_output(&s.output, position);
764+ let scale = s.output.current_scale().fractional_scale();
765+ let area = logical_area(position, mode.size, s.output.current_transform(), scale);
766+ s.id = layout.add_output(name, area);
767+ s.redraw = true;
768+ }
769+ let pos = state.input.pointer_pos;
770+ let held = clamp(state, pos);
771+ if held != pos {
772+ state.warp_pointer(held, InputTime::now());
773+ }
774+ if !parked {
775+ reconfigure_outputs(state);
776+ }
777+ state.reserve_layers();
778+ state.reconfigure_lock();
779+ state.apply_layout();
780+}
781+
730782 impl Drm {
783+ pub fn names(&self) -> Vec<String> {
784+ self.surfaces.values().map(|s| s.output.name()).collect()
785+ }
786+
731787 /// Lights or darkens the output called `name`. Dark outputs get DPMS off
732788 /// and no frames, and the next frame after turning on is a full modeset.
733789 pub fn set_powered(&mut self, name: &str, on: bool) {
Modified file: crates/monolith/src/power.rs+11 additions−4 deletions
@@ -25,8 +25,8 @@ pub struct Power {
2525 lid_closed: bool,
2626 /// The lid close action already ran for this closing.
2727 lid_acted: bool,
28- /// The internal panel is dark because the lid is shut and something else
29- /// is connected.
28+ /// The internal panel is dark and out of the layout because the lid is
29+ /// shut and something else is connected.
3030 panel_off: bool,
3131 last_activity: u64,
3232 timer: Option<RegistrationToken>,
@@ -48,7 +48,7 @@ impl Power {
4848 }
4949 }
5050
51-fn internal(name: &str) -> bool {
51+pub fn internal(name: &str) -> bool {
5252 ["eDP-", "LVDS-", "DSI-"]
5353 .iter()
5454 .any(|p| name.starts_with(p))
@@ -100,14 +100,21 @@ impl State {
100100
101101 /// Pushes the wanted power state of every output to the backend.
102102 pub fn apply_power(&mut self) {
103- let names: Vec<String> = self.wm.space.outputs().map(|o| o.name()).collect();
103+ let names: Vec<String> = match self.backend.drm() {
104+ Some(drm) => drm.names(),
105+ None => self.wm.space.outputs().map(|o| o.name()).collect(),
106+ };
104107 let power = &mut self.services.power;
105108 power.panel_off = power.lid_closed && names.iter().any(|n| !internal(n));
109+ let parked = power.panel_off;
106110 for name in names {
107111 let on = self.output_powered(&name);
108112 if let Some(drm) = self.backend.drm_mut() {
109113 drm.set_powered(&name, on);
110114 }
115+ if internal(&name) {
116+ crate::drm::set_parked(self, &name, parked);
117+ }
111118 }
112119 let power = &self.services.power;
113120 self.protocols.output_power.sync(|o| !power.dark(&o.name()));