event_loop: Separate and use for physical keyboard presence
Extra included: Change of naked Submission pointers to Wrapped.
This commit is contained in:
141
src/event_loop/driver.rs
Normal file
141
src/event_loop/driver.rs
Normal file
@ -0,0 +1,141 @@
|
||||
/* Copyright (C) 2021 Purism SPC
|
||||
* SPDX-License-Identifier: GPL-3.0+
|
||||
*/
|
||||
|
||||
/*! This drives the loop from the `loop` module.
|
||||
*
|
||||
* The tracker loop needs to be driven somehow,
|
||||
* and connected to the external world,
|
||||
* both on the side of receiving and sending events.
|
||||
*
|
||||
* That's going to be implementation-dependent,
|
||||
* connecting to some external mechanisms
|
||||
* for time, messages, and threading/callbacks.
|
||||
*
|
||||
* This is the "imperative shell" part of the software,
|
||||
* and no longer unit-testable.
|
||||
*/
|
||||
|
||||
use crate::event_loop;
|
||||
use crate::logging;
|
||||
use crate::main::Commands;
|
||||
use crate::state::{ Application, Event };
|
||||
use glib;
|
||||
use std::sync::mpsc;
|
||||
use std::thread;
|
||||
use std::time::Instant;
|
||||
|
||||
// Traits
|
||||
use crate::logging::Warn;
|
||||
|
||||
|
||||
type Sender = mpsc::Sender<Event>;
|
||||
type UISender = glib::Sender<Commands>;
|
||||
|
||||
/// This loop driver spawns a new thread which updates the state in a loop,
|
||||
/// in response to incoming events.
|
||||
/// It sends outcomes to the glib main loop using a channel.
|
||||
/// The outcomes are applied by the UI end of the channel in the `main` module.
|
||||
// This could still be reasonably tested,
|
||||
// by creating a glib::Sender and checking what messages it receives.
|
||||
#[derive(Clone)]
|
||||
pub struct Threaded {
|
||||
thread: Sender,
|
||||
}
|
||||
|
||||
impl Threaded {
|
||||
pub fn new(ui: UISender, initial_state: Application) -> Self {
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let saved_sender = sender.clone();
|
||||
thread::spawn(move || {
|
||||
let mut state = event_loop::State::new(initial_state, Instant::now());
|
||||
loop {
|
||||
match receiver.recv() {
|
||||
Ok(event) => {
|
||||
state = Self::handle_loop_event(&sender, state, event, &ui);
|
||||
},
|
||||
Err(e) => {
|
||||
logging::print(logging::Level::Bug, &format!("Senders hung up, aborting: {}", e));
|
||||
return;
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
thread: saved_sender,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send(&self, event: Event) -> Result<(), mpsc::SendError<Event>> {
|
||||
self.thread.send(event)
|
||||
}
|
||||
|
||||
fn handle_loop_event(loop_sender: &Sender, state: event_loop::State, event: Event, ui: &UISender)
|
||||
-> event_loop::State
|
||||
{
|
||||
let now = Instant::now();
|
||||
|
||||
let (new_state, commands) = event_loop::handle_event(state.clone(), event, now);
|
||||
|
||||
ui.send(commands)
|
||||
.or_warn(&mut logging::Print, logging::Problem::Bug, "Can't send to UI");
|
||||
|
||||
if new_state.scheduled_wakeup != state.scheduled_wakeup {
|
||||
if let Some(when) = new_state.scheduled_wakeup {
|
||||
Self::schedule_timeout_wake(loop_sender, when);
|
||||
}
|
||||
}
|
||||
|
||||
new_state
|
||||
}
|
||||
|
||||
fn schedule_timeout_wake(loop_sender: &Sender, when: Instant) {
|
||||
let sender = loop_sender.clone();
|
||||
thread::spawn(move || {
|
||||
let now = Instant::now();
|
||||
thread::sleep(when - now);
|
||||
sender.send(Event::TimeoutReached(when))
|
||||
.or_warn(&mut logging::Print, logging::Problem::Warning, "Can't wake visibility manager");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// For calling in only
|
||||
mod c {
|
||||
use super::*;
|
||||
|
||||
use crate::state::Presence;
|
||||
use crate::state::visibility;
|
||||
use crate::util::c::Wrapped;
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C"
|
||||
fn squeek_state_send_force_visible(mgr: Wrapped<Threaded>) {
|
||||
let sender = mgr.clone_ref();
|
||||
let sender = sender.borrow();
|
||||
sender.send(Event::Visibility(visibility::Event::ForceVisible))
|
||||
.or_warn(&mut logging::Print, logging::Problem::Warning, "Can't send to state manager");
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C"
|
||||
fn squeek_state_send_force_hidden(sender: Wrapped<Threaded>) {
|
||||
let sender = sender.clone_ref();
|
||||
let sender = sender.borrow();
|
||||
sender.send(Event::Visibility(visibility::Event::ForceHidden))
|
||||
.or_warn(&mut logging::Print, logging::Problem::Warning, "Can't send to state manager");
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C"
|
||||
fn squeek_state_send_keyboard_present(sender: Wrapped<Threaded>, present: u32) {
|
||||
let sender = sender.clone_ref();
|
||||
let sender = sender.borrow();
|
||||
let state =
|
||||
if present == 0 { Presence::Missing }
|
||||
else { Presence::Present };
|
||||
sender.send(Event::PhysicalKeyboard(state))
|
||||
.or_warn(&mut logging::Print, logging::Problem::Warning, "Can't send to state manager");
|
||||
}
|
||||
}
|
||||
186
src/event_loop/mod.rs
Normal file
186
src/event_loop/mod.rs
Normal file
@ -0,0 +1,186 @@
|
||||
/* Copyright (C) 2021 Purism SPC
|
||||
* SPDX-License-Identifier: GPL-3.0+
|
||||
*/
|
||||
|
||||
/*! The loop abstraction for driving state changes.
|
||||
* It binds to the state tracker in `state::Application`,
|
||||
* and actually gets driven by a driver in the `driver` module.
|
||||
*
|
||||
* * * *
|
||||
*
|
||||
* If we performed updates in a tight loop,
|
||||
* the state tracker would have been all we need.
|
||||
*
|
||||
* ``
|
||||
* loop {
|
||||
* event = current_event()
|
||||
* outcome = update_state(event)
|
||||
* io.apply(outcome)
|
||||
* }
|
||||
* ``
|
||||
*
|
||||
* This is enough to process all events,
|
||||
* and keep the window always in sync with the current state.
|
||||
*
|
||||
* However, we're trying to be conservative,
|
||||
* and not waste time performing updates that don't change state,
|
||||
* so we have to react to events that end up influencing the state.
|
||||
*
|
||||
* One complication from that is that animation steps
|
||||
* are not a response to events coming from the owner of the loop,
|
||||
* but are needed by the loop itself.
|
||||
*
|
||||
* This is where the rest of bugs hide:
|
||||
* too few scheduled wakeups mean missed updates and wrong visible state.
|
||||
* Too many wakeups can slow down the process, or make animation jittery.
|
||||
* The loop iteration is kept as a pure function to stay testable.
|
||||
*/
|
||||
|
||||
pub mod driver;
|
||||
|
||||
// This module is tightly coupled to the shape of data passed around in this project.
|
||||
// That's not a problem as long as there's only one loop.
|
||||
// They can still be abstracted into Traits,
|
||||
// and the loop parametrized over them.
|
||||
use crate::main::Commands;
|
||||
use crate::state;
|
||||
use crate::state::Event;
|
||||
use std::cmp;
|
||||
use std::time::{ Duration, Instant };
|
||||
|
||||
|
||||
/// This keeps the state of the tracker loop between iterations
|
||||
#[derive(Clone)]
|
||||
struct State {
|
||||
state: state::Application,
|
||||
scheduled_wakeup: Option<Instant>,
|
||||
last_update: Instant,
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn new(initial_state: state::Application, now: Instant) -> Self {
|
||||
Self {
|
||||
state: initial_state,
|
||||
scheduled_wakeup: None,
|
||||
last_update: now,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A single iteration of the loop, updating its persistent state.
|
||||
/// - updates tracker state,
|
||||
/// - determines outcome,
|
||||
/// - determines next scheduled animation wakeup,
|
||||
/// and because this is a pure function, it's easily testable.
|
||||
/// It returns the new state, and the message to send onwards.
|
||||
fn handle_event(
|
||||
mut loop_state: State,
|
||||
event: Event,
|
||||
now: Instant,
|
||||
) -> (State, Commands) {
|
||||
// Calculate changes to send to the consumer,
|
||||
// based on publicly visible state.
|
||||
// The internal state may change more often than the publicly visible one,
|
||||
// so the resulting changes may be no-ops.
|
||||
let old_state = loop_state.state.clone();
|
||||
let last_update = loop_state.last_update;
|
||||
loop_state.state = loop_state.state.apply_event(event.clone(), now);
|
||||
loop_state.last_update = now;
|
||||
|
||||
let new_outcome = loop_state.state.get_outcome(now);
|
||||
|
||||
let commands = old_state.get_outcome(last_update)
|
||||
.get_commands_to_reach(&new_outcome);
|
||||
|
||||
// Timeout events are special: they affect the scheduled timeout.
|
||||
loop_state.scheduled_wakeup = match event {
|
||||
Event::TimeoutReached(when) => {
|
||||
if when > now {
|
||||
// Special handling for scheduled events coming in early.
|
||||
// Wait at least 10 ms to avoid Zeno's paradox.
|
||||
// This is probably not needed though,
|
||||
// if the `now` contains the desired time of the event.
|
||||
// But then what about time "reversing"?
|
||||
Some(cmp::max(
|
||||
when,
|
||||
now + Duration::from_millis(10),
|
||||
))
|
||||
} else {
|
||||
// There's only one timeout in flight, and it's this one.
|
||||
// It's about to complete, and then the tracker can be cleared.
|
||||
// I'm not sure if this is strictly necessary.
|
||||
None
|
||||
}
|
||||
},
|
||||
_ => loop_state.scheduled_wakeup.clone(),
|
||||
};
|
||||
|
||||
// Reschedule timeout if the new state calls for it.
|
||||
let scheduled = &loop_state.scheduled_wakeup;
|
||||
let desired = loop_state.state.get_next_wake(now);
|
||||
|
||||
loop_state.scheduled_wakeup = match (scheduled, desired) {
|
||||
(&Some(scheduled), Some(next)) => {
|
||||
if scheduled > next {
|
||||
// State wants a wake to happen before the one which is already scheduled.
|
||||
// The previous state is removed in order to only ever keep one in flight.
|
||||
// That hopefully avoids pileups,
|
||||
// e.g. because the system is busy
|
||||
// and the user keeps doing something that queues more events.
|
||||
Some(next)
|
||||
} else {
|
||||
// Not changing the case when the wanted wake is *after* scheduled,
|
||||
// because wakes are not expensive as long as they don't pile up,
|
||||
// and I can't see a pileup potential when it doesn't retrigger itself.
|
||||
// Skipping an expected event is much more dangerous.
|
||||
Some(scheduled)
|
||||
}
|
||||
},
|
||||
(None, Some(next)) => Some(next),
|
||||
// No need to change the unneeded wake - see above.
|
||||
// (Some(_), None) => ...
|
||||
(other, _) => other.clone(),
|
||||
};
|
||||
|
||||
(loop_state, commands)
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::animation;
|
||||
use crate::imservice::{ ContentHint, ContentPurpose };
|
||||
use crate::main::PanelCommand;
|
||||
use crate::state::{ Application, InputMethod, InputMethodDetails, Presence, visibility };
|
||||
|
||||
fn imdetails_new() -> InputMethodDetails {
|
||||
InputMethodDetails {
|
||||
purpose: ContentPurpose::Normal,
|
||||
hint: ContentHint::NONE,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn schedule_hide() {
|
||||
let start = Instant::now(); // doesn't matter when. It would be better to have a reproducible value though
|
||||
let mut now = start;
|
||||
|
||||
let state = Application {
|
||||
im: InputMethod::Active(imdetails_new()),
|
||||
physical_keyboard: Presence::Missing,
|
||||
visibility_override: visibility::State::NotForced,
|
||||
};
|
||||
|
||||
let l = State::new(state, now);
|
||||
let (l, commands) = handle_event(l, InputMethod::InactiveSince(now).into(), now);
|
||||
assert_eq!(commands.panel_visibility, Some(PanelCommand::Show));
|
||||
assert_eq!(l.scheduled_wakeup, Some(now + animation::HIDING_TIMEOUT));
|
||||
|
||||
now += animation::HIDING_TIMEOUT;
|
||||
|
||||
let (l, commands) = handle_event(l, Event::TimeoutReached(now), now);
|
||||
assert_eq!(commands.panel_visibility, Some(PanelCommand::Hide));
|
||||
assert_eq!(l.scheduled_wakeup, None);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user