Files
squeekboard/src/panel.rs
2023-03-15 15:44:55 +00:00

383 lines
13 KiB
Rust

/* Copyright (C) 2022 Purism SPC
* SPDX-License-Identifier: GPL-3.0+
*/
/*! Panel state management.
*
* This is effectively a mirror of the previous C code,
* with an explicit state machine managing the panel size.
*
* It still relies on a callback from Wayland to accept the panel size,
* which makes this code somewhat prone to mistakes.
*
* An alternative to the callback would be
* to send a message all the way to `state::State`
* every time the allocated size changes.
* That would allow for a more holistic view
* of interactions of different pieces of state.
*
* However, `state::State` already has the potential to become a ball of mud,
* tightly coupling different functionality and making it difficult to see independent units.
*
* For this reason, I'm taking a light touch approach with the panel manager,
* and moving it just a bit closer to `state::State`.
* Hopefully ths still allows us to expose assumptions that were not stated yet
* (e.g. can the output disappear between size request andallocation?).
*
* Tight coupling, e.g. a future one between presented hints and layout size,
* will have to be taken into account later.
*/
use crate::logging;
use crate::outputs::OutputId;
use crate::util::c::Wrapped;
pub mod c {
use super::*;
use glib;
use std::os::raw::c_void;
use crate::outputs::c::WlOutput;
/// struct panel_manager*
#[repr(transparent)]
#[derive(Clone, Copy)]
pub struct PanelManager(*const c_void);
extern "C" {
#[allow(improper_ctypes)]
pub fn panel_manager_request_widget(
service: PanelManager,
output: WlOutput,
height: u32,
// for callbacks
panel: Wrapped<Manager>,
);
pub fn panel_manager_resize(service: PanelManager, height: u32);
pub fn panel_manager_hide(service: PanelManager);
}
#[no_mangle]
pub extern "C"
fn squeek_panel_manager_configured(panel: Wrapped<Manager>, width: u32, height: u32) {
// This is why this needs to be moved into state::State:
// it's getting too coupled to glib.
glib::idle_add_local(move || {
let panel = panel.clone_ref();
panel.borrow_mut().set_configured(Size{width, height});
glib::Continue(false)
});
}
}
/// Size in pixels that is aware of scaling
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct PixelSize {
pub pixels: u32,
pub scale_factor: u32,
}
fn div_ceil(a: u32, b: u32) -> u32 {
// Given that it's for pixels on a screen, an overflow is unlikely.
(a + b - 1) / b
}
impl PixelSize {
pub fn as_scaled_floor(&self) -> u32 {
self.pixels / self.scale_factor
}
pub fn as_scaled_ceiling(&self) -> u32 {
div_ceil(self.pixels, self.scale_factor)
}
}
#[derive(Clone, Debug, PartialEq)]
struct Size {
width: u32,
height: u32,
}
/// This state requests the Wayland layer shell protocol synchronization:
/// the application asks for some size,
/// and then receives a size that the compositor thought appropriate.
/// Stores raw values passed to Wayland, i.e. scaled dimensions.
#[derive(Clone, Debug, PartialEq)]
enum State {
Hidden,
SizeRequested {
output: OutputId,
height: u32,
//width: u32,
},
SizeAllocated {
output: OutputId,
wanted_height: u32,
allocated: Size,
},
}
/// A command to send out to the next layer of processing.
/// Here, it's the C side of the panel.
#[derive(Debug, PartialEq)]
enum Update {
Hide,
Resize { height: u32 },
RequestWidget {
output: OutputId,
height: u32,
},
}
#[derive(Clone, PartialEq, Debug)]
pub enum Command {
Show {
output: OutputId,
height: PixelSize,
},
Hide,
}
/// Tries to contain all the panel sizing duties.
pub struct Manager {
panel: c::PanelManager,
state: State,
// This should be part of State, if it ever actually gets unhardcoded.
// It's here because State doesn't need to become more complex
// until this becomes properly used.
debug: bool,
}
impl Manager {
pub fn new(panel: c::PanelManager) -> Self {
Self {
panel,
state: State::Hidden,
debug: false,
}
}
// TODO: mabe send the allocated size back to state::State,
// to perform layout adjustments
fn set_configured(&mut self, size: Size) {
if self.debug {
eprintln!("Panel received configure {:?}", &size);
}
self.state = self.state.clone().configure(size);
if self.debug {
eprintln!("Panel now {:?}", &self.state);
}
}
pub fn update(mgr: Wrapped<Manager>, cmd: Command) {
let copied = mgr.clone();
let mgr = mgr.clone_ref();
let mut mgr = mgr.borrow_mut();
if mgr.debug {
eprintln!("Panel received {:?}", &cmd);
}
let (state, updates) = mgr.state.clone().update(cmd);
(*mgr).state = state;
for update in &updates {
unsafe {
match update {
Update::Hide => c::panel_manager_hide(mgr.panel),
Update::Resize { height }
=> c::panel_manager_resize(mgr.panel, *height),
Update::RequestWidget{output, height}
=> c::panel_manager_request_widget(mgr.panel, output.0, *height, copied.clone()),
}
}
}
if mgr.debug {
for update in &updates {
eprintln!("Panel updates: {:?}", &update);
}
eprintln!("Panel is now {:?}", &(*mgr).state);
}
}
}
impl State {
fn configure(self, size: Size) -> Self {
match self {
State::Hidden => {
// This may happen if a hide is scheduled immediately after a show.
log_print!(
logging::Level::Surprise,
"Panel has been configured, but no request is pending. Ignoring",
);
State::Hidden
},
State::SizeAllocated{output, wanted_height, ..} => {
log_print!(
logging::Level::Surprise,
"Panel received new configuration without asking",
);
State::SizeAllocated{output, wanted_height, allocated: size}
},
State::SizeRequested{output, height} => State::SizeAllocated {
output,
wanted_height: height,
allocated: size,
},
}
}
fn update(self, cmd: Command) -> (Self, Vec<Update>) {
match (cmd, self) {
(Command::Hide, State::Hidden) => (State::Hidden, Vec::new()),
(Command::Hide, State::SizeAllocated{..}) => (
State::Hidden, vec![Update::Hide],
),
(Command::Hide, State::SizeRequested{..}) => (
State::Hidden, vec![Update::Hide],
),
(Command::Show{output, height}, State::Hidden) => {
let height = height.as_scaled_ceiling();
(
State::SizeRequested{output, height},
vec![Update::RequestWidget{ output, height }],
)
},
(
Command::Show{output, height},
State::SizeRequested{output: req_output, height: req_height},
) => {
let height = height.as_scaled_ceiling();
if output == req_output && height == req_height {(
State::SizeRequested{output: req_output, height: req_height},
Vec::new(),
)} else if output == req_output {(
// I'm not sure about that.
// This could cause a busy loop,
// when two requests are being processed at the same time:
// one message in the compositor to allocate size A,
// causing the state to update to height A'
// the other from the state wanting height B',
// causing the compositor to change size to B.
// So better cut this short here, despite artifacts.
// Doing nothing means that Squeekboard will occasionally use the stale size (see test),
// so instead always listen to the higher layer and request a new size.
// If this causes problems, maybe count requests/configures, or track what was allocated in response to what request.
State::SizeRequested{output, height},
vec![Update::Resize { height }],
)} else {(
// This looks weird, but should be safe.
// The stack seems to handle
// configure events on a dead surface.
State::SizeRequested{output, height},
vec![
Update::Hide,
Update::RequestWidget { output, height },
],
)}
},
(
Command::Show{output, height},
State::SizeAllocated{output: alloc_output, allocated, wanted_height},
) => {
let height = height.as_scaled_ceiling();
if output == alloc_output && height == wanted_height {(
State::SizeAllocated{output: alloc_output, wanted_height, allocated},
Vec::new(),
)} else if output == alloc_output && height == allocated.height {(
State::SizeAllocated{output: alloc_output, wanted_height: height, allocated},
Vec::new(),
)} else if output == alloc_output {(
// Should *all* other heights cause a resize?
// What about those between wanted and allocated?
State::SizeRequested{output, height},
vec![Update::Resize{height}],
)} else {(
State::SizeRequested{output, height},
vec![
Update::Hide,
Update::RequestWidget{output, height},
]
)}
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::outputs::c::WlOutput;
#[test]
fn resize_before_configured() {
// allow to make typing fields easier
#[allow(non_upper_case_globals)]
const output: OutputId = OutputId(WlOutput::dummy());
let state = State::Hidden;
// Initial show
let (state, cmds) = state.update(Command::Show {
output,
height: PixelSize { pixels: 100, scale_factor: 1 },
});
assert_eq!(
cmds,
vec![Update::RequestWidget { output, height: 100 }],
);
// layer shell requests a resize
// but another show comes before first can be confirmed
let (state, cmds) = dbg!(state).update(Command::Show {
output,
height: PixelSize { pixels: 50, scale_factor: 1 },
});
assert_eq!(
cmds,
vec![Update::Resize { height: 50 }],
"{:?}",
state,
);
// This is too many layers of indirection, but as long as layer shell is tied to gtk widgets, there's not much to be done.
// The main issue is that as the outputs change, we acknowledge the wrong (maintained) size:
/*
[346947.774] wl_output@31.geometry(0, 0, 65, 130, 0, "<Unknown>", "<Unknown>", 3)
[346948.117] wl_output@17.geometry(0, 0, 65, 130, 0, "<Unknown>", "<Unknown>", 3)
[346948.198] zwlr_layer_surface_v1@41.configure(1709, 720, 210)
[346948.268] -> zwlr_layer_surface_v1@41.ack_configure(1709)
*/
// TODO: check if layer_shell allows not acknowledging a configure event, and which part of squeekboard is responsible for that
// (there are no messages in between, so it's not PanelMgr; panel.c almost-unconditionally calls to Rust too; could it be layer-shell.c?).
// event we want
let good_state = state.clone().configure(Size { width: 50, height: 50 });
assert_eq!(
good_state,
State::SizeAllocated {
output,
wanted_height: 50,
allocated: Size { width: 50, height: 50 },
},
);
// or stale event we do not want
let state = state.configure(Size { width: 50, height: 100 });
// followed by the good one
let state = state.configure(Size { width: 50, height: 50 });
assert_eq!(
state,
State::SizeAllocated {
output,
wanted_height: 50,
allocated: Size { width: 50, height: 50 },
},
);
}
}