river_layout_toolkit/
lib.rs

1#![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
15/// This trait represents a layout generator implementation.
16pub trait Layout: 'static {
17    /// The error type of [`user_cmd`](Self::user_cmd) and [`generate_layout`](Self::generate_layout)
18    /// functions. Use [`Infallible`](std::convert::Infallible) if you don't need it.
19    type Error: StdError;
20
21    /// The namespace is used by the compositor to distinguish between layout generators. Two separate
22    /// clients may not share a namespace. Otherwise, [`run`] will return [`Error::NamespaceInUse`].
23    const NAMESPACE: &'static str;
24
25    /// This function is called whenever the user sends a command via `riverctl send-layout-cmd`.
26    ///
27    /// # Errors
28    ///
29    /// An error returned from this function will be logged, but it will not terminate the application.
30    fn user_cmd(&mut self, cmd: String, tags: Option<u32>, output: &str)
31        -> Result<(), Self::Error>;
32
33    /// This function is called whenever compositor requests a layout.
34    ///
35    /// # Errors
36    ///
37    /// Returning an error from this fuction will cause [`run`] to terminate.
38    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}