river_layout_toolkit/
lib.rs1#![warn(clippy::match_same_arms)]
2#![warn(clippy::semicolon_if_nothing_returned)]
3#![warn(clippy::unnecessary_wraps)]
4
5use std::error::Error as StdError;
6use std::ffi::CString;
7use std::io;
8
9use wayrs_client::global::{Global, GlobalExt};
10use wayrs_client::protocol::*;
11use wayrs_client::{Connection, EventCtx, IoMode};
12
13wayrs_client::generate!("river-layout-v3.xml");
14
15pub trait Layout: 'static {
17 type Error: StdError;
20
21 const NAMESPACE: &'static str;
24
25 fn user_cmd(&mut self, cmd: String, tags: Option<u32>, output: &str)
31 -> Result<(), Self::Error>;
32
33 fn generate_layout(
39 &mut self,
40 view_count: u32,
41 usable_width: u32,
42 usable_height: u32,
43 tags: u32,
44 output: &str,
45 ) -> Result<GeneratedLayout, Self::Error>;
46}
47
48#[derive(Debug)]
49pub struct GeneratedLayout {
50 pub layout_name: String,
51 pub views: Vec<Rectangle>,
52}
53
54#[derive(Debug)]
55pub struct Rectangle {
56 pub x: i32,
57 pub y: i32,
58 pub width: u32,
59 pub height: u32,
60}
61
62#[derive(Debug, thiserror::Error)]
63pub enum Error<E: StdError> {
64 #[error("Could not connect to Waylasd: {0}")]
65 WaylandConnect(#[from] wayrs_client::ConnectError),
66 #[error("Unsupported compositor: {0}")]
67 WaylandBind(#[from] wayrs_client::global::BindError),
68 #[error("IO error: {0}")]
69 Io(#[from] io::Error),
70 #[error("Namespace '{0}' is in use")]
71 NamespaceInUse(String),
72 #[error("Invalid generated layout")]
73 InvalidGeneratedLayout,
74 #[error("Layout error: {0}")]
75 LayoutError(E),
76}
77
78pub fn run<L: Layout>(layout: L) -> Result<(), Error<L::Error>> {
79 let mut conn = Connection::connect()?;
80 conn.blocking_roundtrip()?;
81 conn.add_registry_cb(wl_registry_cb);
82
83 let mut state = State {
84 layout_manager: conn.bind_singleton(1..=2)?,
85 last_user_cmd_tags: None,
86 layout,
87 outputs: Vec::new(),
88 error: None,
89 };
90
91 loop {
92 conn.dispatch_events(&mut state);
93 if let Some(err) = state.error.take() {
94 return Err(err);
95 }
96
97 conn.flush(IoMode::Blocking)?;
98 conn.recv_events(IoMode::Blocking)?;
99 }
100}
101
102struct State<L: Layout> {
103 layout_manager: river_layout_manager_v3::RiverLayoutManagerV3,
104 last_user_cmd_tags: Option<u32>,
105 layout: L,
106 outputs: Vec<Output>,
107 error: Option<Error<L::Error>>,
108}
109
110struct Output {
111 wl_output: WlOutput,
112 reg_name: u32,
113 river_layout: Option<RiverLayout>,
114}
115
116struct RiverLayout {
117 river: RiverLayoutV3,
118 output_name: String,
119}
120
121impl Output {
122 fn bind<L: Layout>(conn: &mut Connection<State<L>>, global: &Global) -> Self {
123 Self {
124 wl_output: global.bind_with_cb(conn, 4, wl_output_cb).unwrap(),
125 reg_name: global.name,
126 river_layout: None,
127 }
128 }
129
130 fn drop<L: Layout>(self, conn: &mut Connection<State<L>>) {
131 if let Some(river_layout) = self.river_layout {
132 river_layout.river.destroy(conn);
133 }
134 self.wl_output.release(conn);
135 }
136}
137
138fn wl_registry_cb<L: Layout>(
139 conn: &mut Connection<State<L>>,
140 state: &mut State<L>,
141 event: &wl_registry::Event,
142) {
143 match event {
144 wl_registry::Event::Global(global) if global.is::<WlOutput>() => {
145 state.outputs.push(Output::bind(conn, global));
146 }
147 wl_registry::Event::GlobalRemove(name) => {
148 if let Some(output_index) = state.outputs.iter().position(|o| o.reg_name == *name) {
149 let output = state.outputs.swap_remove(output_index);
150 output.drop(conn);
151 }
152 }
153 _ => (),
154 }
155}
156
157fn wl_output_cb<L: Layout>(ctx: EventCtx<State<L>, WlOutput>) {
158 let output = ctx
159 .state
160 .outputs
161 .iter_mut()
162 .find(|o| o.wl_output == ctx.proxy)
163 .expect("Received event for unknown output");
164
165 if output.river_layout.is_some() {
166 return;
167 }
168
169 if let wl_output::Event::Name(name) = ctx.event {
170 output.river_layout = Some(RiverLayout {
171 river: ctx.state.layout_manager.get_layout_with_cb(
172 ctx.conn,
173 output.wl_output,
174 CString::new(L::NAMESPACE).unwrap(),
175 river_layout_cb,
176 ),
177 output_name: name.into_string().unwrap(),
178 });
179 }
180}
181
182fn river_layout_cb<L: Layout>(ctx: EventCtx<State<L>, RiverLayoutV3>) {
183 use river_layout_v3::Event;
184
185 let layout = ctx
186 .state
187 .outputs
188 .iter()
189 .filter_map(|o| o.river_layout.as_ref())
190 .find(|o| o.river == ctx.proxy)
191 .expect("Received event for unknown layout object");
192
193 match ctx.event {
194 Event::NamespaceInUse => {
195 ctx.state.error = Some(Error::NamespaceInUse(L::NAMESPACE.into()));
196 ctx.conn.break_dispatch_loop();
197 }
198 Event::LayoutDemand(args) => {
199 let generated_layout = match ctx.state.layout.generate_layout(
200 args.view_count,
201 args.usable_width,
202 args.usable_height,
203 args.tags,
204 &layout.output_name,
205 ) {
206 Ok(l) => l,
207 Err(e) => {
208 ctx.state.error = Some(Error::LayoutError(e));
209 ctx.conn.break_dispatch_loop();
210 return;
211 }
212 };
213
214 if generated_layout.views.len() != args.view_count as usize {
215 ctx.state.error = Some(Error::InvalidGeneratedLayout);
216 ctx.conn.break_dispatch_loop();
217 return;
218 }
219
220 for rect in generated_layout.views {
221 layout.river.push_view_dimensions(
222 ctx.conn,
223 rect.x,
224 rect.y,
225 rect.width,
226 rect.height,
227 args.serial,
228 );
229 }
230
231 layout.river.commit(
232 ctx.conn,
233 CString::new(generated_layout.layout_name).unwrap(),
234 args.serial,
235 );
236 }
237 Event::UserCommand(command) => {
238 if let Err(err) = ctx.state.layout.user_cmd(
239 command.into_string().unwrap(),
240 ctx.state.last_user_cmd_tags,
241 &layout.output_name,
242 ) {
243 log::warn!("user_cmd error: {err}");
244 }
245 }
246 Event::UserCommandTags(tags) => {
247 ctx.state.last_user_cmd_tags = Some(tags);
248 }
249 }
250}