Skip to content

Commit 2099451

Browse files
PandorasFoxclaude
andcommitted
Remove lateral scrolling scaffolding.
I wasn't sure if the niri windows IPC update was going to be sufficient for my purposes here - tl;dr it isn't (no events emitted when a column is centered -> not enough information to deduce when to scroll). So, I'm removing all that jazz. Because I had already implemented the relevant bits on the render side (and had previously stubbed out the Niri side to just default-value 50% for the scroll every time on the x-axis), I used claude to munge all the window IPC bits for me to evaluate if it would be a sufficiently pleasing experience. It wasn't, so I cleaned up all the resulting work. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 3f28b79 commit 2099451

File tree

5 files changed

+56
-125
lines changed

5 files changed

+56
-125
lines changed

src/pithos/commands.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@ pub enum RenderMode {
66
// single image
77
Static, // will scale up/down to fill
88
ScrollVertical,
9-
ScrollLateral,
10-
// scrolling both directions will be trickier to implement. later problem.
11-
// hello from later me: honestly it's probably easier than I thought:
12-
// the agent can enforce positional state well, & correcting-on-the-fly looks better than expected
9+
// ScrollLateral, // not using this in niri agent presently; not enough IPC to cleanly animate on
10+
// most of the logic remains in comments; could be useful for other compositors with portrait monitors (?).
1311
}
1412

1513
// ===== COMMAND STRUCTS =====

src/pithos/misc.rs

Lines changed: 1 addition & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -137,15 +137,10 @@ pub fn get_viewport_dimensions(
137137
let viewport_height = (width_ratio * output_height as f64).round() as i32;
138138
(viewport_width, viewport_height)
139139
}
140-
RenderMode::ScrollLateral => {
141-
let viewport_width = (height_ratio * output_width as f64).round() as i32;
142-
let viewport_height = image_height;
143-
(viewport_width, viewport_height)
144-
}
145140
}
146141
}
147142

148-
// i used claude pro to generate these tests, with some nudging about what the expectations were.
143+
// i used Claude to generate these tests, with some nudging about what the expectations were.
149144
// it worked reasonably well and didn't actually take that long, and (after fixing the one test
150145
// that got horizontal and vertical confused), the tests did show my function worked. yippee :)
151146
#[cfg(test)]
@@ -211,22 +206,6 @@ mod tests {
211206
assert!((viewport_ratio - output_ratio).abs() < 0.01);
212207
}
213208

214-
#[test]
215-
fn test_scroll_lateral_mode() {
216-
// In lateral scroll mode, viewport height = image height
217-
// viewport width scaled to maintain output aspect ratio
218-
let (vw, vh) = get_viewport_dimensions(4000, 1500, 1920, 1080, RenderMode::ScrollLateral);
219-
assert_eq!(vh, 1500); // Full image height
220-
let expected_width = 2667;
221-
assert_eq!(vw, expected_width);
222-
assert!(vw <= 4000); // Viewport width <= image width
223-
assert!(vh <= 1500); // Viewport height <= image height
224-
225-
let output_ratio = 1920.0 / 1080.0;
226-
let viewport_ratio = vw as f64 / vh as f64;
227-
assert!((viewport_ratio - output_ratio).abs() < 0.01);
228-
}
229-
230209
#[test]
231210
fn test_laptop_resolutions_with_web_images() {
232211
// 1366x768 laptop with typical web image 1920x1080
@@ -292,15 +271,6 @@ mod tests {
292271
let viewport_ratio = vw as f64 / vh as f64;
293272
assert!((viewport_ratio - output_ratio).abs() < 0.01);
294273

295-
// Lateral scroll: ultrawide with wide panorama
296-
let (vw, vh) = get_viewport_dimensions(12000, 2000, 3440, 1440, RenderMode::ScrollLateral);
297-
assert_eq!(vh, 2000);
298-
assert!(vw <= 12000);
299-
assert!(vh <= 2000);
300-
let output_ratio = 3440.0 / 1440.0;
301-
let viewport_ratio = vw as f64 / vh as f64;
302-
assert!((viewport_ratio - output_ratio).abs() < 0.01);
303-
304274
// Vertical scroll: 4K monitor with very tall image
305275
let (vw, vh) = get_viewport_dimensions(3000, 15000, 3840, 2160, RenderMode::ScrollVertical);
306276
assert_eq!(vw, 3000);

src/threads/lockscreen.rs

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -41,54 +41,7 @@ impl Cerberus {
4141
//self.raise_lockscreen();
4242
//self.listen_for_unlock()
4343
}
44-
/*
45-
get wayland globals:
46-
connection, compositor, shm, outputs, viewport. create globals struct, no optionals, just arcs.
47-
bind ext_session_lock_manager_v1 => acquire session lock - lives in-thread
48-
spawn render threads per output; their state lives in the vec<(output_state, wloutput)> hamburger. they only get connection<state> and path (evaluate cloning this ? it's mut ? fuck)
49-
primary lock thread continues on to input parsing, sending to.... oh god, the channels.
5044

51-
what if we do this all in one thread? what if we just process input, then dispatch events for frame callbacks?
52-
=> nothing can block ever
53-
54-
so. we need to *also* put the receivers in the output state, and arc them, on output::done during the initial callback hell.
55-
56-
we also need to make sure that we *cleanly* handle the callbacks for mode changes. We'll want to send that
57-
into its own callback, which can evaluate the proper actions to take - i believe we will want to destroy the active surface,
58-
and create a new lock_surface
59-
60-
... so we need a way to ask the input threa-.
61-
..... so we need a main thread that holds the input thread, the ext_session_lock, and the output render threads.
62-
... sounds familiar.
63-
..... (clutches my head)
64-
.... so. the main thread will grab everything that's needed. it will hold the connection, the globals, etc.
65-
... it will acquire the lock, _and then_ spawn the thread that handles input, and the lockscreen thread.
66-
... the lockscreen thread will be responsible for managing the surfaces attached to a given WlOutput.
67-
.... it will manage the state (attached image, buffer, which lockscreen(s) gets the indicators) with its config blob
68-
=> how do we get the current lockscreen viewport state for ourselves?
69-
==> we should be able to fetch the offsets from the agent at lock time
70-
===> niri agent needs a refactor to have a .getState {output: (image, mode, offset)} (and an updateState.... yeah....)
71-
... it will dispatch events for frame draws
72-
... it will read its event queue from the input thread
73-
... LastAction Instance::now whenever an event from event queue => reset state
74-
... it will use subsurfaces to separate wallpaper bottom layer from (cairo?) canvas upper layer
75-
... it will have appropriate handling of when the callback necessitates surface recreation (and wloutput object remapping in the vec as well i presume, oh god)
76-
... overall actually sounds quite pleasant
77-
... the main thread will wait for the AUTH_FINISH response from lock thread
78-
... IMMEDIATELY unlock and destroy() the lock. "OBJECTS CREATED ARE STILL VALID"
79-
... upon which it passes that on to the lockscreen thread (exits gracefully) => up the surfaces and releases them all => we rejoice
80-
... because the underlying compositor block should be gone, we can maybe opacity lighten? hmmm? eyewiggles?
81-
... OPACITY LIGHTEN AND ZOOM IN MAYBE?? :eyes:
82-
83-
okay yeah that's enough thinking on that
84-
85-
methods accessible on the 'handler' (cerberus) should be defined by trait so pandora (hecate ? ) can reuse
86-
87-
88-
and im gonna write it all by hand because this is a security sensitive application and i wish to take it slowly and carefully!
89-
(so i can reuse the well written independent functions for the normal render threads/architecture)
90-
hggggh
91-
*/
9245
fn _log(&self, msg: String) {
9346
self._do_log(LogLevel::DEFAULT, format!("[lockscreen] {msg}"));
9447
}

src/threads/niri.rs

Lines changed: 51 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ struct OutputState {
119119
current_image: String,
120120
mode: Option<RenderMode>,
121121
max_workspace_idx: u8,
122+
active_workspace_idx: Option<u8>,
122123
}
123124

124125
#[derive(Default)]
@@ -128,13 +129,6 @@ struct NiriProcessor {
128129
workspaces: Vec<Workspace>,
129130
}
130131

131-
/*
132-
TODO: refactor/implement lateral scrolling position within workspaces
133-
handle window change events to (re)calculate scrolling positions of workspaces
134-
plumb scroll command generation to use desired workspace scrolling position
135-
136-
*/
137-
138132
impl NiriProcessor {
139133
fn update_config(
140134
&mut self,
@@ -144,33 +138,54 @@ impl NiriProcessor {
144138
for new_output_conf in &new_config.outputs {
145139
let p = pandora.clone();
146140
let new_mode = new_output_conf.mode.unwrap_or(RenderMode::Static);
147-
let (output_name, state) = match self
148-
.outputs
149-
.iter_mut()
150-
.find(|o| o.0 == new_output_conf.name)
151-
{
152-
Some(v) => v,
153-
None => continue,
141+
142+
// First, find the output and check if we need to update
143+
let needs_update = if let Some((_, state)) = self.outputs.iter().find(|o| o.0 == new_output_conf.name) {
144+
state.current_image != new_output_conf.image || state.mode.unwrap_or(RenderMode::Static) != new_mode
145+
} else {
146+
continue; // Output not found
154147
};
155-
if state.current_image != new_output_conf.image
156-
|| state.mode.unwrap_or(RenderMode::Static) != new_mode
157-
{
148+
149+
if needs_update {
150+
let position = self.get_current_position_for_output(&new_output_conf.name, new_mode);
158151
let cmd = RenderCommand {
159-
output: output_name.clone(),
152+
output: new_output_conf.name.clone(),
160153
image: new_output_conf.image.clone(),
161154
mode: new_mode,
162-
position: (0.0, 0.0), // todo: should compute scroll position + plumb this on the other end
155+
position,
163156
};
164157
p.handle_cmd(&CommandType::Tc(RenderThreadCommand::Render(cmd)));
165158

166159
// Update state to reflect the change
167-
state.current_image = new_output_conf.image.clone();
168-
state.mode = Some(new_mode);
160+
if let Some((_, state)) = self.outputs.iter_mut().find(|o| o.0 == new_output_conf.name) {
161+
state.current_image = new_output_conf.image.clone();
162+
state.mode = Some(new_mode);
163+
}
169164
}
170165
}
171166
self.config = new_config;
172167
}
173168

169+
fn get_current_position_for_output(&self, output_name: &str, mode: RenderMode) -> (f64, f64) {
170+
match mode {
171+
RenderMode::Static => (0.0, 0.0),
172+
RenderMode::ScrollVertical => {
173+
// Use existing vertical scroll logic
174+
let output = match self.outputs.iter().find(|o| o.0 == output_name) {
175+
Some((_, state)) => state,
176+
None => return (0.0, 0.0),
177+
};
178+
179+
let active_workspace_idx = output.active_workspace_idx.unwrap_or(1);
180+
let mut scroll_percent = 100.0 * (active_workspace_idx - 1) as f64 / (output.max_workspace_idx - 1) as f64;
181+
if scroll_percent.is_nan() {
182+
scroll_percent = 50.0;
183+
}
184+
(50.0, scroll_percent)
185+
}
186+
}
187+
}
188+
174189
fn update_workspaces(&mut self, workspaces: &Vec<Workspace>) {
175190
for workspace in workspaces {
176191
if workspace.output.is_some() {
@@ -181,8 +196,14 @@ impl NiriProcessor {
181196
};
182197
let cur_max_idx = output_state.1.max_workspace_idx;
183198
output_state.1.max_workspace_idx = u8::max(workspace.idx, cur_max_idx);
199+
200+
// Update active workspace index
201+
if workspace.is_active {
202+
output_state.1.active_workspace_idx = Some(workspace.idx);
203+
}
184204
}
185205
}
206+
186207
self.workspaces = workspaces.clone();
187208
}
188209

@@ -205,15 +226,17 @@ impl NiriProcessor {
205226
current_image: img_path,
206227
mode: output_config.mode,
207228
max_workspace_idx: 0,
229+
active_workspace_idx: None,
208230
};
209231
self.outputs.push((output_name.clone(), output_state));
210232
}
211233
}
212234
self.update_workspaces(&workspaces);
235+
213236
}
214237

215-
fn reseat_scroll_positions(&self, pandora: Arc<dyn Daemon + Send + Sync>) {
216-
for workspace in &self.workspaces {
238+
fn reseat_scroll_positions(&mut self, pandora: Arc<dyn Daemon + Send + Sync>) {
239+
for workspace in &self.workspaces.clone() {
217240
if workspace.is_active {
218241
self.gen_scroll_cmd_for_workspace_id(pandora.clone(), workspace.id);
219242
}
@@ -228,21 +251,16 @@ impl NiriProcessor {
228251
output.1.max_workspace_idx = 0;
229252
}
230253
self.update_workspaces(&workspaces);
254+
231255
self.reseat_scroll_positions(pandora.clone());
232256
}
233257
Event::WorkspaceActivated { id, .. } => {
234258
self.gen_scroll_cmd_for_workspace_id(pandora.clone(), id)
235259
}
236-
Event::WindowFocusChanged { id: _ } => {
237-
self.poke(pandora.clone());
238-
// TODO - niri includes tile layouts in WindowLayout structs now
239-
// we should keep track of the full pixel width of each workspace,
240-
// as well as the position of the focused window within that mosaic
241-
// and compute a scroll percentage based on that
242-
// we'll want to then start using that whenever we gen_scroll_cmd,
243-
// and just trust the render thread to discard or use as needed.
244-
}
245-
Event::WindowLayoutsChanged { changes } => {
260+
//Event::WindowFocusChanged { id: _id } => {
261+
// self.poke(pandora.clone());
262+
//}
263+
Event::WindowLayoutsChanged { changes: _changes } => {
246264
self.poke(pandora.clone());
247265
}
248266
_ => (), // idc about other events rn
@@ -287,10 +305,6 @@ impl NiriProcessor {
287305
});
288306
Some(CommandType::Tc(cmd))
289307
}
290-
Some(RenderMode::ScrollLateral) => {
291-
// TODO: implement horizontal scrolling for window changes
292-
todo!()
293-
}
294308
Some(RenderMode::Static) => None,
295309
} {
296310
pandora.verbose("niri-agent", format!("emitting command {cmd:?}"));

src/wayland/render_base.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,7 @@ impl WallpaperRenderState {
439439
pub fn scroll(
440440
&mut self,
441441
conn: &mut Connection<RenderThreadState>,
442-
scroll_x: f64,
442+
_scroll_x: f64,
443443
scroll_y: f64,
444444
) {
445445
// scroll (x|y) track the % (0.0 => 100.0) of the center of the viewport along each dimension
@@ -450,7 +450,6 @@ impl WallpaperRenderState {
450450
match self.mode {
451451
RenderMode::Static => return,
452452
RenderMode::ScrollVertical => self.height.scroll(scroll_y, self.slowdown),
453-
RenderMode::ScrollLateral => self.width.scroll(scroll_x, self.slowdown),
454453
};
455454

456455
if !is_already_scrolling {
@@ -757,19 +756,16 @@ fn image_to_file(
757756
width: u32,
758757
height: u32,
759758
) -> (i32, i32) {
759+
// note: should wrap this and fall back to static if vert won't work, or infer it ourselves
760760
let scale_to = match mode {
761761
RenderMode::Static => (Some(width), Some(height)),
762762
RenderMode::ScrollVertical => (Some(width), None),
763-
RenderMode::ScrollLateral => (None, Some(height)),
764763
};
765764

766765
pandora.clone().load_image(path).unwrap();
767766
let (img_width, img_height) = pandora.clone().read_img_to_file(path, f, scale_to).unwrap();
768767

769768
if img_width < width || img_height < height {
770-
// TODO: better handling of this case
771-
// idealy coerce to static at runtime and just log
772-
// im lazy for now tho
773769
panic!(
774770
"INVALID CONFIG COMBINATION: image scaled to {img_width} x {img_height}, but output is {width} by {height}.\nSwitch to STATIC mode for this image or try scrolling in the other direction."
775771
)

0 commit comments

Comments
 (0)