event_loop: Separate and use for physical keyboard presence

Extra included: Change of naked Submission pointers to Wrapped.
This commit is contained in:
Dorota Czaplejewicz
2021-12-03 14:29:02 +00:00
parent 53137fd2e2
commit 4cc7017e1c
19 changed files with 953 additions and 774 deletions

View File

@ -1,13 +0,0 @@
#pragma once
#include <gtk/gtk.h>
// from main.h
struct sender;
// from animations.rs
struct squeek_animation_visibility_manager;
struct squeek_animation_visibility_manager *squeek_animation_visibility_manager_new(struct sender *ui_sender);
void squeek_animation_visibility_manager_send_claim_visible(struct squeek_animation_visibility_manager *animman);
void squeek_animation_visibility_manager_send_force_hide(struct squeek_animation_visibility_manager *animman);

View File

@ -2,441 +2,16 @@
* SPDX-License-Identifier: GPL-3.0+ * SPDX-License-Identifier: GPL-3.0+
*/ */
/*! Animation state trackers and drivers. /*! Animation details */
* Concerns the presentation layer.
*
* Documentation and comments in this module
* are meant to be read from the top to bottom. */
use crate::logging;
use glib;
use std::cmp;
use std::sync::mpsc;
use std::time::{ Duration, Instant };
// Traits
use crate::logging::Warn;
use std::time::Duration;
/// The keyboard should hide after this has elapsed to prevent flickering. /// The keyboard should hide after this has elapsed to prevent flickering.
const HIDING_TIMEOUT: Duration = Duration::from_millis(200); pub const HIDING_TIMEOUT: Duration = Duration::from_millis(200);
/// Events that the state tracker processes
#[derive(Clone)]
pub enum Event {
ClaimVisible,
/// The panel is not needed
ReleaseVisible,
/// The user requested the panel to go down
ForceHide,
/// Event triggered because a moment in time passed.
/// Use to animate state transitions.
/// The value is the ideal arrival time.
TimeoutReached(Instant),
}
/// The outwardly visible state of visibility /// The outwardly visible state of visibility
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug, Clone)]
pub enum Outcome { pub enum Outcome {
Visible, Visible,
Hidden, Hidden,
} }
/// The actual logic of visibility animation.
/// It keeps the pael visible for a short time period after each hide request.
/// This prevents flickering on quick successive enable/disable events.
/// It does not treat user-driven hiding in a special way.
///
/// This is the "functional core".
/// All state changes return the next state and the optimal time for the next check.
///
/// This state tracker can be driven by any event loop.
#[derive(Clone, PartialEq, Debug)]
enum VisibilityTracker {
Visible,
/// Wait until the instant is reached and then hide immediately.
/// Depending on the relation to current time, it means either visible or hidden.
HiddenAfter(Instant),
}
use self::VisibilityTracker::*;
impl VisibilityTracker {
fn apply_event(self, event: Event, now: Instant) -> Self {
match event {
Event::ClaimVisible => Visible,
Event::ReleaseVisible => match self {
Visible => HiddenAfter(now + HIDING_TIMEOUT),
other => other,
},
Event::ForceHide => match self {
// Special case to avoid unneeded state changes.
HiddenAfter(when) => HiddenAfter(cmp::min(when, now)),
_ => HiddenAfter(now),
},
// The tracker doesn't change just because time is passing.
Event::TimeoutReached(_) => self,
}
}
/// Returns the state visible to the outside
fn get_outcome(&self, now: Instant) -> Outcome {
let visible = match self {
Visible => true,
HiddenAfter(hide_after) => *hide_after > now,
};
if visible {
Outcome::Visible
} else {
Outcome::Hidden
}
}
/// Returns the next time to update the state.
fn get_next_wake(&self, now: Instant) -> Option<Instant> {
match self {
HiddenAfter(next) => {
if *next > now { Some(*next) }
else { None }
},
_ => None,
}
}
}
/* If we performed updates in a tight loop,
* the Tracker would have been all we need.
*
* loop {
* event = current_event()
* outcome = update_state(event)
* window.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.
*/
/// This keeps the state of the tracker loop between iterations
#[derive(Clone)]
struct LoopState {
state: VisibilityTracker,
scheduled_wakeup: Option<Instant>,
}
impl LoopState {
fn new(initial_state: VisibilityTracker) -> Self {
Self {
state: initial_state,
scheduled_wakeup: None,
}
}
}
/// 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 optional message to send onwards.
fn handle_loop_event(
mut loop_state: LoopState,
event: Event,
now: Instant,
) -> (LoopState, Option<Outcome>) {
// Forward current public state to the consumer.
// This doesn't take changes into account,
// but we're only sending updates as a response to events,
// so no-ops shouldn't dominate.
loop_state.state = loop_state.state.apply_event(event.clone(), now);
let outcome = loop_state.state.get_outcome(now);
// 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, Some(outcome))
}
/*
* 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 std::thread;
type Sender = mpsc::Sender<Event>;
type UISender = glib::Sender<Outcome>;
/// 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.
// This could still be reasonably tested,
// by creating a glib::Sender and checking what messages it receives.
#[derive(Clone)]
pub struct ThreadLoopDriver {
thread: Sender,
}
impl ThreadLoopDriver {
pub fn new(ui: UISender) -> Self {
let (sender, receiver) = mpsc::channel();
let saved_sender = sender.clone();
thread::spawn(move || {
let mut state = LoopState::new(VisibilityTracker::Visible);
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: LoopState, event: Event, ui: &UISender) -> LoopState {
let now = Instant::now();
let (new_state, outcome) = handle_loop_event(state.clone(), event, now);
if let Some(outcome) = outcome {
ui.send(outcome)
.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 util::c::{ ArcWrapped, Wrapped };
#[no_mangle]
pub extern "C"
fn squeek_animation_visibility_manager_new(sender: ArcWrapped<UISender>)
-> Wrapped<ThreadLoopDriver>
{
let sender = sender.clone_ref();
let sender = sender.lock().unwrap();
Wrapped::new(ThreadLoopDriver::new(sender.clone()))
}
#[no_mangle]
pub extern "C"
fn squeek_animation_visibility_manager_send_claim_visible(mgr: Wrapped<ThreadLoopDriver>) {
let sender = mgr.clone_ref();
let sender = sender.borrow();
sender.send(Event::ClaimVisible)
.or_warn(&mut logging::Print, logging::Problem::Warning, "Can't send to visibility manager");
}
#[no_mangle]
pub extern "C"
fn squeek_animation_visibility_manager_send_force_hide(sender: Wrapped<ThreadLoopDriver>) {
let sender = sender.clone_ref();
let sender = sender.borrow();
sender.send(Event::ForceHide)
.or_warn(&mut logging::Print, logging::Problem::Warning, "Can't send to visibility manager");
}
}
#[cfg(test)]
mod test {
use super::*;
/// Test the original delay scenario: no flicker on quick switches.
#[test]
fn hide_show() {
let start = Instant::now(); // doesn't matter when. It would be better to have a reproducible value though
let mut now = start;
let state = VisibilityTracker::Visible;
let state = state.apply_event(Event::ReleaseVisible, now);
// Check 100ms at 1ms intervals. It should remain visible.
for _i in 0..100 {
now += Duration::from_millis(1);
assert_eq!(
state.get_outcome(now),
Outcome::Visible,
"Hidden when it should remain visible: {:?}",
now.saturating_duration_since(start),
)
}
let state = state.apply_event(Event::ClaimVisible, now);
assert_eq!(state.get_outcome(now), Outcome::Visible);
}
/// Make sure that hiding works when requested legitimately
#[test]
fn 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 = VisibilityTracker::Visible;
let state = state.apply_event(Event::ReleaseVisible, now);
while let Outcome::Visible = state.get_outcome(now) {
now += Duration::from_millis(1);
assert!(
now < start + Duration::from_millis(250),
"Hiding too slow: {:?}",
now.saturating_duration_since(start),
);
}
}
/// Check against the false showing bug.
/// Expectation: it will get hidden and not appear again
#[test]
fn false_show() {
let start = Instant::now(); // doesn't matter when. It would be better to have a reproducible value though
let mut now = start;
let state = VisibilityTracker::Visible;
// This reflects the sequence from Wayland:
// disable, disable, enable, disable
// all in a single batch.
let state = state.apply_event(Event::ReleaseVisible, now);
let state = state.apply_event(Event::ReleaseVisible, now);
let state = state.apply_event(Event::ClaimVisible, now);
let state = state.apply_event(Event::ReleaseVisible, now);
while let Outcome::Visible = state.get_outcome(now) {
now += Duration::from_millis(1);
assert!(
now < start + Duration::from_millis(250),
"Still not hidden: {:?}",
now.saturating_duration_since(start),
);
}
// One second without appearing again
for _i in 0..1000 {
now += Duration::from_millis(1);
assert_eq!(
state.get_outcome(now),
Outcome::Hidden,
"Appeared unnecessarily: {:?}",
now.saturating_duration_since(start),
);
}
}
#[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 l = LoopState::new(VisibilityTracker::Visible);
let (l, outcome) = handle_loop_event(l, Event::ReleaseVisible, now);
assert_eq!(outcome, Some(Outcome::Visible));
assert_eq!(l.scheduled_wakeup, Some(now + HIDING_TIMEOUT));
now += HIDING_TIMEOUT;
let (l, outcome) = handle_loop_event(l, Event::TimeoutReached(now), now);
assert_eq!(outcome, Some(Outcome::Hidden));
assert_eq!(l.scheduled_wakeup, None);
}
}

View File

@ -19,7 +19,9 @@
#include "config.h" #include "config.h"
#include "dbus.h" #include "dbus.h"
#include "main.h"
#include <inttypes.h>
#include <stdio.h> #include <stdio.h>
#include <gio/gio.h> #include <gio/gio.h>
@ -53,9 +55,9 @@ handle_set_visible(SmPuriOSK0 *object, GDBusMethodInvocation *invocation,
DBusHandler *service = user_data; DBusHandler *service = user_data;
if (arg_visible) { if (arg_visible) {
squeek_animation_visibility_manager_send_claim_visible (service->animman); squeek_state_send_force_visible (service->state_manager);
} else { } else {
squeek_animation_visibility_manager_send_force_hide (service->animman); squeek_state_send_force_hidden(service->state_manager);
} }
sm_puri_osk0_complete_set_visible(object, invocation); sm_puri_osk0_complete_set_visible(object, invocation);
@ -65,12 +67,12 @@ handle_set_visible(SmPuriOSK0 *object, GDBusMethodInvocation *invocation,
DBusHandler * DBusHandler *
dbus_handler_new (GDBusConnection *connection, dbus_handler_new (GDBusConnection *connection,
const gchar *object_path, const gchar *object_path,
struct squeek_animation_visibility_manager *animman) struct squeek_state_manager *state_manager)
{ {
DBusHandler *self = calloc(1, sizeof(DBusHandler)); DBusHandler *self = calloc(1, sizeof(DBusHandler));
self->object_path = g_strdup(object_path); self->object_path = g_strdup(object_path);
self->connection = connection; self->connection = connection;
self->animman = animman; self->state_manager = state_manager;
self->dbus_interface = sm_puri_osk0_skeleton_new(); self->dbus_interface = sm_puri_osk0_skeleton_new();
g_signal_connect(self->dbus_interface, "handle-set-visible", g_signal_connect(self->dbus_interface, "handle-set-visible",

View File

@ -19,10 +19,11 @@
#ifndef DBUS_H_ #ifndef DBUS_H_
#define DBUS_H_ 1 #define DBUS_H_ 1
#include "animation.h"
#include "sm.puri.OSK0.h" #include "sm.puri.OSK0.h"
// From main.h
struct squeek_state_manager;
G_BEGIN_DECLS G_BEGIN_DECLS
#define DBUS_SERVICE_PATH "/sm/puri/OSK0" #define DBUS_SERVICE_PATH "/sm/puri/OSK0"
@ -41,12 +42,12 @@ typedef struct _DBusHandler
char *object_path; char *object_path;
/// Forward incoming events there /// Forward incoming events there
struct squeek_animation_visibility_manager *animman; // shared reference struct squeek_state_manager *state_manager; // shared reference
} DBusHandler; } DBusHandler;
DBusHandler * dbus_handler_new (GDBusConnection *connection, DBusHandler * dbus_handler_new (GDBusConnection *connection,
const gchar *object_path, const gchar *object_path,
struct squeek_animation_visibility_manager *animman); struct squeek_state_manager *state_manager);
void dbus_handler_destroy(DBusHandler*); void dbus_handler_destroy(DBusHandler*);
G_END_DECLS G_END_DECLS

141
src/event_loop/driver.rs Normal file
View 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
View 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);
}
}

View File

@ -23,22 +23,6 @@ static const struct zwp_input_method_v2_listener input_method_listener = {
.unavailable = imservice_handle_unavailable, .unavailable = imservice_handle_unavailable,
}; };
struct submission* get_submission(struct zwp_input_method_manager_v2 *immanager,
struct zwp_virtual_keyboard_manager_v1 *vkmanager,
struct vis_manager *vis_manager,
struct wl_seat *seat,
EekboardContextService *state) {
struct zwp_input_method_v2 *im = NULL;
if (immanager) {
im = zwp_input_method_manager_v2_get_input_method(immanager, seat);
}
struct zwp_virtual_keyboard_v1 *vk = NULL;
if (vkmanager) {
vk = zwp_virtual_keyboard_manager_v1_create_virtual_keyboard(vkmanager, seat);
}
return submission_new(im, vk, state, vis_manager);
}
/// Un-inlined /// Un-inlined
struct zwp_input_method_v2 *imservice_manager_get_input_method(struct zwp_input_method_manager_v2 *manager, struct zwp_input_method_v2 *imservice_manager_get_input_method(struct zwp_input_method_manager_v2 *manager,
struct wl_seat *seat) { struct wl_seat *seat) {

View File

@ -8,7 +8,11 @@ use std::ffi::CString;
use std::fmt; use std::fmt;
use std::num::Wrapping; use std::num::Wrapping;
use std::string::String; use std::string::String;
use std::time::Instant;
use crate::event_loop::driver;
use crate::state;
use crate::state::Event;
use ::logging; use ::logging;
use ::util::c::into_cstring; use ::util::c::into_cstring;
@ -23,8 +27,6 @@ pub mod c {
use std::os::raw::{c_char, c_void}; use std::os::raw::{c_char, c_void};
pub use ::submission::c::StateManager;
// The following defined in C // The following defined in C
/// struct zwp_input_method_v2* /// struct zwp_input_method_v2*
@ -39,7 +41,6 @@ pub mod c {
pub fn eek_input_method_commit_string(im: *mut InputMethod, text: *const c_char); pub fn eek_input_method_commit_string(im: *mut InputMethod, text: *const c_char);
pub fn eek_input_method_delete_surrounding_text(im: *mut InputMethod, before: u32, after: u32); pub fn eek_input_method_delete_surrounding_text(im: *mut InputMethod, before: u32, after: u32);
pub fn eek_input_method_commit(im: *mut InputMethod, serial: u32); pub fn eek_input_method_commit(im: *mut InputMethod, serial: u32);
fn eekboard_context_service_set_hint_purpose(state: *const StateManager, hint: u32, purpose: u32);
} }
// The following defined in Rust. TODO: wrap naked pointers to Rust data inside RefCells to prevent multiple writers // The following defined in Rust. TODO: wrap naked pointers to Rust data inside RefCells to prevent multiple writers
@ -140,7 +141,6 @@ pub mod c {
im: *const InputMethod) im: *const InputMethod)
{ {
let imservice = check_imservice(imservice, im).unwrap(); let imservice = check_imservice(imservice, im).unwrap();
let active_changed = imservice.current.active ^ imservice.pending.active;
imservice.current = imservice.pending.clone(); imservice.current = imservice.pending.clone();
imservice.pending = IMProtocolState { imservice.pending = IMProtocolState {
@ -149,19 +149,7 @@ pub mod c {
}; };
imservice.serial += Wrapping(1u32); imservice.serial += Wrapping(1u32);
imservice.send_event();
if active_changed {
(imservice.active_callback)(imservice.current.active);
if imservice.current.active {
unsafe {
eekboard_context_service_set_hint_purpose(
imservice.state_manager,
imservice.current.content_hint.bits(),
imservice.current.content_purpose.clone() as u32,
);
}
}
}
} }
// TODO: this is really untested // TODO: this is really untested
@ -177,7 +165,7 @@ pub mod c {
// the keyboard is already decommissioned // the keyboard is already decommissioned
imservice.current.active = false; imservice.current.active = false;
(imservice.active_callback)(imservice.current.active); imservice.send_event();
} }
// FIXME: destroy and deallocate // FIXME: destroy and deallocate
@ -328,9 +316,7 @@ impl Default for IMProtocolState {
pub struct IMService { pub struct IMService {
/// Owned reference (still created and destroyed in C) /// Owned reference (still created and destroyed in C)
pub im: *mut c::InputMethod, pub im: *mut c::InputMethod,
/// Unowned reference. Be careful, it's shared with C at large sender: driver::Threaded,
state_manager: *const c::StateManager,
active_callback: Box<dyn Fn(bool)>,
pending: IMProtocolState, pending: IMProtocolState,
current: IMProtocolState, // turn current into an idiomatic representation? current: IMProtocolState, // turn current into an idiomatic representation?
@ -346,15 +332,13 @@ pub enum SubmitError {
impl IMService { impl IMService {
pub fn new( pub fn new(
im: *mut c::InputMethod, im: *mut c::InputMethod,
state_manager: *const c::StateManager, sender: driver::Threaded,
active_callback: Box<dyn Fn(bool)>,
) -> Box<IMService> { ) -> Box<IMService> {
// IMService will be referenced to by C, // IMService will be referenced to by C,
// so it needs to stay in the same place in memory via Box // so it needs to stay in the same place in memory via Box
let imservice = Box::new(IMService { let imservice = Box::new(IMService {
im, im,
active_callback, sender,
state_manager,
pending: IMProtocolState::default(), pending: IMProtocolState::default(),
current: IMProtocolState::default(), current: IMProtocolState::default(),
preedit_string: String::new(), preedit_string: String::new(),
@ -414,4 +398,21 @@ impl IMService {
pub fn is_active(&self) -> bool { pub fn is_active(&self) -> bool {
self.current.active self.current.active
} }
fn send_event(&self) {
let state = &self.current;
let timestamp = Instant::now();
let message = if state.active {
state::InputMethod::Active(
state::InputMethodDetails {
hint: state.content_hint,
purpose: state.content_purpose,
}
)
} else {
state::InputMethod::InactiveSince(timestamp)
};
self.sender.send(Event::InputMethod(message))
.or_warn(&mut logging::Print, logging::Problem::Warning, "Can't send to state manager");
}
} }

View File

@ -22,6 +22,7 @@ mod action;
mod animation; mod animation;
pub mod data; pub mod data;
mod drawing; mod drawing;
mod event_loop;
pub mod float_ord; pub mod float_ord;
pub mod imservice; pub mod imservice;
mod keyboard; mod keyboard;
@ -32,6 +33,7 @@ mod manager;
mod outputs; mod outputs;
mod popover; mod popover;
mod resources; mod resources;
mod state;
mod style; mod style;
mod submission; mod submission;
pub mod tests; pub mod tests;

View File

@ -1,17 +1,33 @@
#pragma once #pragma once
/// This all wraps https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/struct.MainContext.html#method.channel /// This all wraps https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/struct.MainContext.html#method.channel
#include <inttypes.h>
#include "input-method-unstable-v2-client-protocol.h"
#include "virtual-keyboard-unstable-v1-client-protocol.h"
#include "eek/eek-types.h" #include "eek/eek-types.h"
#include "dbus.h" #include "dbus.h"
struct receiver;
struct sender;
struct channel { struct receiver;
struct sender *sender;
/// Wrapped<event_loop::driver::Threaded>
struct squeek_state_manager;
struct submission;
struct rsobjects {
struct receiver *receiver; struct receiver *receiver;
struct squeek_state_manager *state_manager;
struct submission *submission;
}; };
/// Creates a channel with one end inside the glib main loop
struct channel main_loop_channel_new(void);
void register_ui_loop_handler(struct receiver *receiver, ServerContextService *ui, DBusHandler *dbus_handler); void register_ui_loop_handler(struct receiver *receiver, ServerContextService *ui, DBusHandler *dbus_handler);
struct rsobjects squeek_rsobjects_new(struct zwp_input_method_v2 *im, struct zwp_virtual_keyboard_v1 *vk);
void squeek_state_send_force_visible(struct squeek_state_manager *state);
void squeek_state_send_force_hidden(struct squeek_state_manager *state);
void squeek_state_send_keyboard_present(struct squeek_state_manager *state, uint32_t keyboard_present);

View File

@ -4,17 +4,23 @@
/*! Glue for the main loop. */ /*! Glue for the main loop. */
use crate::animation::Outcome as Message; use crate::state;
use glib::{Continue, MainContext, PRIORITY_DEFAULT, Receiver, Sender}; use glib::{Continue, MainContext, PRIORITY_DEFAULT, Receiver};
use std::thread;
use std::time::Duration;
mod c { mod c {
use super::*; use super::*;
use std::os::raw::c_void; use std::os::raw::c_void;
use std::rc::Rc; use std::rc::Rc;
use std::time::Instant;
use ::util::c::{ ArcWrapped, Wrapped }; use crate::event_loop::driver;
use crate::imservice::IMService;
use crate::imservice::c::InputMethod;
use crate::state;
use crate::submission::Submission;
use crate::util::c::Wrapped;
use crate::vkeyboard::c::ZwpVirtualKeyboardV1;
/// ServerContextService* /// ServerContextService*
#[repr(transparent)] #[repr(transparent)]
@ -24,56 +30,56 @@ mod c {
#[repr(transparent)] #[repr(transparent)]
pub struct DBusHandler(*const c_void); pub struct DBusHandler(*const c_void);
/// Corresponds to main.c::channel /// Holds the Rust structures that are interesting from C.
#[repr(C)] #[repr(C)]
pub struct Channel { pub struct RsObjects {
sender: ArcWrapped<Sender<Message>>, receiver: Wrapped<Receiver<Commands>>,
receiver: Wrapped<Receiver<Message>>, state_manager: Wrapped<driver::Threaded>,
submission: Wrapped<Submission>,
} }
extern "C" { extern "C" {
pub fn server_context_service_real_show_keyboard(imservice: *const UIManager); fn server_context_service_real_show_keyboard(service: *const UIManager);
pub fn server_context_service_real_hide_keyboard(imservice: *const UIManager); fn server_context_service_real_hide_keyboard(service: *const UIManager);
fn server_context_service_set_hint_purpose(service: *const UIManager, hint: u32, purpose: u32);
// This should probably only get called from the gtk main loop, // This should probably only get called from the gtk main loop,
// given that dbus handler is using glib. // given that dbus handler is using glib.
pub fn dbus_handler_set_visible(dbus: *const DBusHandler, visible: u8); fn dbus_handler_set_visible(dbus: *const DBusHandler, visible: u8);
} }
/// Creates what's possible in Rust to eliminate as many FFI calls as possible,
/// because types aren't getting checked across their boundaries,
/// and that leads to suffering.
#[no_mangle] #[no_mangle]
pub extern "C" pub extern "C"
fn main_loop_channel_new() -> Channel { fn squeek_rsobjects_new(
im: *mut InputMethod,
vk: ZwpVirtualKeyboardV1,
) -> RsObjects {
let (sender, receiver) = MainContext::channel(PRIORITY_DEFAULT); let (sender, receiver) = MainContext::channel(PRIORITY_DEFAULT);
let sender = ArcWrapped::new(sender);
let receiver = Wrapped::new(receiver); let now = Instant::now();
let channel = Channel { let state_manager = driver::Threaded::new(sender, state::Application::new(now));
sender,
receiver, let imservice = if im.is_null() {
None
} else {
Some(IMService::new(im, state_manager.clone()))
}; };
let submission = Submission::new(vk, imservice);
//start_work(channel.sender.clone()); RsObjects {
submission: Wrapped::new(submission),
channel state_manager: Wrapped::new(state_manager),
} receiver: Wrapped::new(receiver),
}
/// testing only
fn start_work(sender: ArcWrapped<Sender<Message>>) {
let sender = sender.clone_ref();
thread::spawn(move || {
let sender = sender.lock().unwrap();
thread::sleep(Duration::from_secs(3));
sender.send(Message::Visible).unwrap();
thread::sleep(Duration::from_secs(3));
sender.send(Message::Hidden).unwrap();
thread::sleep(Duration::from_secs(3));
sender.send(Message::Visible).unwrap();
});
} }
/// Places the UI loop callback in the glib main loop. /// Places the UI loop callback in the glib main loop.
#[no_mangle] #[no_mangle]
pub extern "C" pub extern "C"
fn register_ui_loop_handler( fn register_ui_loop_handler(
receiver: Wrapped<Receiver<Message>>, receiver: Wrapped<Receiver<Commands>>,
ui_manager: *const UIManager, ui_manager: *const UIManager,
dbus_handler: *const DBusHandler, dbus_handler: *const DBusHandler,
) { ) {
@ -97,23 +103,47 @@ mod c {
/// This is the outest layer of the imperative shell, /// This is the outest layer of the imperative shell,
/// and doesn't lend itself to testing other than integration. /// and doesn't lend itself to testing other than integration.
fn main_loop_handle_message( fn main_loop_handle_message(
msg: Message, msg: Commands,
ui_manager: *const UIManager, ui_manager: *const UIManager,
dbus_handler: *const DBusHandler, dbus_handler: *const DBusHandler,
) { ) {
match msg { match msg.panel_visibility {
Message::Visible => unsafe { Some(PanelCommand::Show) => unsafe {
// FIXME: reset layout to default if no IM field is active
// Ideally: anim state stores the current IM hints,
// Message::Visible(hints) is received here
// and applied to layout
server_context_service_real_show_keyboard(ui_manager); server_context_service_real_show_keyboard(ui_manager);
dbus_handler_set_visible(dbus_handler, 1);
}, },
Message::Hidden => unsafe { Some(PanelCommand::Hide) => unsafe {
server_context_service_real_hide_keyboard(ui_manager); server_context_service_real_hide_keyboard(ui_manager);
dbus_handler_set_visible(dbus_handler, 0);
}, },
None => {},
}; };
if let Some(visible) = msg.dbus_visible_set {
unsafe { dbus_handler_set_visible(dbus_handler, visible as u8) };
}
if let Some(hints) = msg.layout_hint_set {
unsafe {
server_context_service_set_hint_purpose(
ui_manager,
hints.hint.bits(),
hints.purpose.clone() as u32,
)
};
}
} }
} }
#[derive(Clone, PartialEq, Debug)]
pub enum PanelCommand {
Show,
Hide,
}
/// The commands consumed by the main loop,
/// to be sent out to external components.
#[derive(Clone)]
pub struct Commands {
pub panel_visibility: Option<PanelCommand>,
pub layout_hint_set: Option<state::InputMethodDetails>,
pub dbus_visible_set: Option<bool>,
}

View File

@ -42,7 +42,7 @@ struct _ServerContextService {
struct submission *submission; // unowned struct submission *submission; // unowned
struct squeek_layout_state *layout; struct squeek_layout_state *layout;
struct ui_manager *manager; // unowned struct ui_manager *manager; // unowned
struct vis_manager *vis_manager; // owned struct squeek_state_manager *state_manager; // shared reference
PhoshLayerSurface *window; PhoshLayerSurface *window;
GtkWidget *widget; // nullable GtkWidget *widget; // nullable
@ -203,6 +203,7 @@ make_widget (ServerContextService *self)
gtk_widget_show_all(self->widget); gtk_widget_show_all(self->widget);
} }
// Called from rust
void void
server_context_service_real_show_keyboard (ServerContextService *self) server_context_service_real_show_keyboard (ServerContextService *self)
{ {
@ -215,17 +216,13 @@ server_context_service_real_show_keyboard (ServerContextService *self)
gtk_widget_show (GTK_WIDGET(self->window)); gtk_widget_show (GTK_WIDGET(self->window));
} }
// Called from rust
void void
server_context_service_real_hide_keyboard (ServerContextService *self) server_context_service_real_hide_keyboard (ServerContextService *self)
{ {
gtk_widget_hide (GTK_WIDGET(self->window)); if (self->window) {
} gtk_widget_hide (GTK_WIDGET(self->window));
}
static void
server_context_service_set_physical_keyboard_present (ServerContextService *self, gboolean physical_keyboard_present)
{
g_return_if_fail (SERVER_IS_CONTEXT_SERVICE (self));
squeek_visman_set_keyboard_present(self->vis_manager, physical_keyboard_present);
} }
static void static void
@ -238,7 +235,7 @@ server_context_service_set_property (GObject *object,
switch (prop_id) { switch (prop_id) {
case PROP_ENABLED: case PROP_ENABLED:
server_context_service_set_physical_keyboard_present (self, !g_value_get_boolean (value)); squeek_state_send_keyboard_present(self->state_manager, !g_value_get_boolean (value));
break; break;
default: default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
@ -321,14 +318,20 @@ init (ServerContextService *self) {
} }
ServerContextService * ServerContextService *
server_context_service_new (EekboardContextService *self, struct submission *submission, struct squeek_layout_state *layout, struct ui_manager *uiman, struct vis_manager *visman) server_context_service_new (EekboardContextService *self, struct submission *submission, struct squeek_layout_state *layout, struct ui_manager *uiman, struct squeek_state_manager *state_manager)
{ {
ServerContextService *ui = g_object_new (SERVER_TYPE_CONTEXT_SERVICE, NULL); ServerContextService *ui = g_object_new (SERVER_TYPE_CONTEXT_SERVICE, NULL);
ui->submission = submission; ui->submission = submission;
ui->state = self; ui->state = self;
ui->layout = layout; ui->layout = layout;
ui->manager = uiman; ui->manager = uiman;
ui->vis_manager = visman; ui->state_manager = state_manager;
init(ui); init(ui);
return ui; return ui;
} }
// Used from Rust
void server_context_service_set_hint_purpose(ServerContextService *self, uint32_t hint,
uint32_t purpose) {
eekboard_context_service_set_hint_purpose(self->state, hint, purpose);
}

View File

@ -29,7 +29,7 @@ G_BEGIN_DECLS
/** Manages the lifecycle of the window displaying layouts. */ /** Manages the lifecycle of the window displaying layouts. */
G_DECLARE_FINAL_TYPE (ServerContextService, server_context_service, SERVER, CONTEXT_SERVICE, GObject) G_DECLARE_FINAL_TYPE (ServerContextService, server_context_service, SERVER, CONTEXT_SERVICE, GObject)
ServerContextService *server_context_service_new(EekboardContextService *self, struct submission *submission, struct squeek_layout_state *layout, struct ui_manager *uiman, struct vis_manager *visman); ServerContextService *server_context_service_new(EekboardContextService *self, struct submission *submission, struct squeek_layout_state *layout, struct ui_manager *uiman, struct squeek_state_manager *state_manager);
enum squeek_arrangement_kind server_context_service_get_layout_type(ServerContextService *); enum squeek_arrangement_kind server_context_service_get_layout_type(ServerContextService *);
void server_context_service_force_show_keyboard (ServerContextService *self); void server_context_service_force_show_keyboard (ServerContextService *self);
void server_context_service_hide_keyboard (ServerContextService *self); void server_context_service_hide_keyboard (ServerContextService *self);

View File

@ -25,7 +25,6 @@
#include "config.h" #include "config.h"
#include "animation.h"
#include "eek/eek.h" #include "eek/eek.h"
#include "eekboard/eekboard-context-service.h" #include "eekboard/eekboard-context-service.h"
#include "dbus.h" #include "dbus.h"
@ -47,15 +46,18 @@ typedef enum _SqueekboardDebugFlags {
} SqueekboardDebugFlags; } SqueekboardDebugFlags;
/// Global application state /// Some state, some IO components, all mixed together.
/// Better move what's possible to state::Application,
/// or secondary data structures of the same general shape.
struct squeekboard { struct squeekboard {
struct squeek_wayland wayland; // Just hooks. struct squeek_wayland wayland; // Just hooks.
DBusHandler *dbus_handler; // Controls visibility of the OSK. DBusHandler *dbus_handler; // Controls visibility of the OSK.
EekboardContextService *settings_context; // Gsettings hooks. EekboardContextService *settings_context; // Gsettings hooks.
ServerContextService *ui_context; // mess, includes the entire UI ServerContextService *ui_context; // mess, includes the entire UI
struct submission *submission; // Wayland text input handling. /// Currently wanted layout. TODO: merge into state::Application
struct squeek_layout_state layout_choice; // Currently wanted layout. struct squeek_layout_state layout_choice;
struct ui_manager *ui_manager; // UI shape tracker/chooser. TODO: merge with layuot choice /// UI shape tracker/chooser. TODO: merge into state::Application
struct ui_manager *ui_manager;
}; };
@ -282,6 +284,21 @@ phosh_theme_init (void)
g_object_set (G_OBJECT (gtk_settings), "gtk-application-prefer-dark-theme", TRUE, NULL); g_object_set (G_OBJECT (gtk_settings), "gtk-application-prefer-dark-theme", TRUE, NULL);
} }
/// Create Rust objects in one go,
/// to avoid crossing the language barrier and losing type information
static struct rsobjects create_rsobjects(struct zwp_input_method_manager_v2 *immanager,
struct zwp_virtual_keyboard_manager_v1 *vkmanager,
struct wl_seat *seat) {
struct zwp_input_method_v2 *im = NULL;
if (immanager) {
im = zwp_input_method_manager_v2_get_input_method(immanager, seat);
}
struct zwp_virtual_keyboard_v1 *vk = NULL;
if (vkmanager) {
vk = zwp_virtual_keyboard_manager_v1_create_virtual_keyboard(vkmanager, seat);
}
return squeek_rsobjects_new(im, vk);
}
static GDebugKey debug_keys[] = static GDebugKey debug_keys[] =
{ {
@ -377,10 +394,9 @@ main (int argc, char **argv)
g_warning("Wayland input method interface not available"); g_warning("Wayland input method interface not available");
} }
struct rsobjects rsobjects = create_rsobjects(instance.wayland.input_method_manager,
struct channel ui_channel = main_loop_channel_new(); instance.wayland.virtual_keyboard_manager,
instance.wayland.seat);
struct squeek_animation_visibility_manager *animman = squeek_animation_visibility_manager_new(ui_channel.sender);
instance.ui_manager = squeek_uiman_new(); instance.ui_manager = squeek_uiman_new();
@ -401,7 +417,7 @@ main (int argc, char **argv)
guint owner_id = 0; guint owner_id = 0;
DBusHandler *service = NULL; DBusHandler *service = NULL;
if (connection) { if (connection) {
service = dbus_handler_new(connection, DBUS_SERVICE_PATH, animman); service = dbus_handler_new(connection, DBUS_SERVICE_PATH, rsobjects.state_manager);
if (service == NULL) { if (service == NULL) {
g_printerr ("Can't create dbus server\n"); g_printerr ("Can't create dbus server\n");
@ -422,38 +438,30 @@ main (int argc, char **argv)
} }
} }
struct vis_manager *vis_manager = squeek_visman_new(animman); eekboard_context_service_set_submission(instance.settings_context, rsobjects.submission);
instance.submission = get_submission(instance.wayland.input_method_manager,
instance.wayland.virtual_keyboard_manager,
vis_manager,
instance.wayland.seat,
instance.settings_context);
eekboard_context_service_set_submission(instance.settings_context, instance.submission);
ServerContextService *ui_context = server_context_service_new( ServerContextService *ui_context = server_context_service_new(
instance.settings_context, instance.settings_context,
instance.submission, rsobjects.submission,
&instance.layout_choice, &instance.layout_choice,
instance.ui_manager, instance.ui_manager,
vis_manager); rsobjects.state_manager);
if (!ui_context) { if (!ui_context) {
g_error("Could not initialize GUI"); g_error("Could not initialize GUI");
exit(1); exit(1);
} }
instance.ui_context = ui_context; instance.ui_context = ui_context;
register_ui_loop_handler(ui_channel.receiver, instance.ui_context, instance.dbus_handler); register_ui_loop_handler(rsobjects.receiver, instance.ui_context, instance.dbus_handler);
session_register(); session_register();
if (debug_flags & SQUEEKBOARD_DEBUG_FLAG_FORCE_SHOW) {
server_context_service_force_show_keyboard (ui_context);
}
if (debug_flags & SQUEEKBOARD_DEBUG_FLAG_GTK_INSPECTOR) { if (debug_flags & SQUEEKBOARD_DEBUG_FLAG_GTK_INSPECTOR) {
gtk_window_set_interactive_debugging (TRUE); gtk_window_set_interactive_debugging (TRUE);
} }
if (debug_flags & SQUEEKBOARD_DEBUG_FLAG_FORCE_SHOW) {
squeek_state_send_force_visible (rsobjects.state_manager);
}
loop = g_main_loop_new (NULL, FALSE); loop = g_main_loop_new (NULL, FALSE);
g_main_loop_run (loop); g_main_loop_run (loop);

417
src/state.rs Normal file
View File

@ -0,0 +1,417 @@
/* Copyright (C) 2021 Purism SPC
* SPDX-License-Identifier: GPL-3.0+
*/
/*! Application-wide state is stored here.
* It's driven by the loop defined in the loop module. */
use crate::animation;
use crate::imservice::{ ContentHint, ContentPurpose };
use crate::main::{ Commands, PanelCommand };
use std::time::Instant;
#[derive(Clone, Copy)]
pub enum Presence {
Present,
Missing,
}
#[derive(Clone)]
pub struct InputMethodDetails {
pub hint: ContentHint,
pub purpose: ContentPurpose,
}
#[derive(Clone)]
pub enum InputMethod {
Active(InputMethodDetails),
InactiveSince(Instant),
}
/// Incoming events
#[derive(Clone)]
pub enum Event {
InputMethod(InputMethod),
Visibility(visibility::Event),
PhysicalKeyboard(Presence),
/// Event triggered because a moment in time passed.
/// Use to animate state transitions.
/// The value is the ideal arrival time.
TimeoutReached(Instant),
}
impl From<InputMethod> for Event {
fn from(im: InputMethod) -> Self {
Self::InputMethod(im)
}
}
pub mod visibility {
#[derive(Clone)]
pub enum Event {
/// User requested the panel to show
ForceVisible,
/// The user requested the panel to go down
ForceHidden,
}
#[derive(Clone, PartialEq, Debug, Copy)]
pub enum State {
/// Last interaction was user forcing the panel to go visible
ForcedVisible,
/// Last interaction was user forcing the panel to hide
ForcedHidden,
/// Last interaction was the input method changing active state
NotForced,
}
}
/// The outwardly visible state.
#[derive(Clone)]
pub struct Outcome {
pub visibility: animation::Outcome,
pub im: InputMethod,
}
impl Outcome {
/// Returns the commands needed to apply changes as required by the new state.
/// This implementation doesn't actually take the old state into account,
/// instead issuing all the commands as needed to reach the new state.
/// The receivers of the commands bear the burden
/// of checking if the commands end up being no-ops.
pub fn get_commands_to_reach(&self, new_state: &Self) -> Commands {
let layout_hint_set = match new_state {
Outcome {
visibility: animation::Outcome::Visible,
im: InputMethod::Active(hints),
} => Some(hints.clone()),
Outcome {
visibility: animation::Outcome::Visible,
im: InputMethod::InactiveSince(_),
} => Some(InputMethodDetails {
hint: ContentHint::NONE,
purpose: ContentPurpose::Normal,
}),
Outcome {
visibility: animation::Outcome::Hidden,
..
} => None,
};
let (dbus_visible_set, panel_visibility) = match new_state.visibility {
animation::Outcome::Visible => (Some(true), Some(PanelCommand::Show)),
animation::Outcome::Hidden => (Some(false), Some(PanelCommand::Hide)),
};
Commands {
panel_visibility,
layout_hint_set,
dbus_visible_set,
}
}
}
/// The actual logic of the program.
/// At this moment, limited to calculating visibility and IM hints.
///
/// It keeps the panel visible for a short time period after each hide request.
/// This prevents flickering on quick successive enable/disable events.
/// It does not treat user-driven hiding in a special way.
///
/// This is the "functional core".
/// All state changes return the next state and the optimal time for the next check.
///
/// This state tracker can be driven by any event loop.
#[derive(Clone)]
pub struct Application {
pub im: InputMethod,
pub visibility_override: visibility::State,
pub physical_keyboard: Presence,
}
impl Application {
/// A conservative default, ignoring the actual state of things.
/// It will initially show the keyboard for a blink.
// The ignorance might actually be desired,
// as it allows for startup without waiting for a system check.
// The downside is that adding actual state should not cause transitions.
// Another acceptable alternative is to allow explicitly uninitialized parts.
pub fn new(now: Instant) -> Self {
Self {
im: InputMethod::InactiveSince(now),
visibility_override: visibility::State::NotForced,
physical_keyboard: Presence::Missing,
}
}
pub fn apply_event(self, event: Event, _now: Instant) -> Self {
match event {
Event::TimeoutReached(_) => self,
Event::Visibility(visibility) => Self {
visibility_override: match visibility {
visibility::Event::ForceHidden => visibility::State::ForcedHidden,
visibility::Event::ForceVisible => visibility::State::ForcedVisible,
},
..self
},
Event::PhysicalKeyboard(presence) => Self {
physical_keyboard: presence,
..self
},
Event::InputMethod(new_im) => match (self.im.clone(), new_im) {
(InputMethod::Active(_old), InputMethod::Active(new_im))
=> Self {
im: InputMethod::Active(new_im),
..self
},
// For changes in active state, remove user's visibility override.
// Both cases spelled out explicitly, rather than by the wildcard,
// to not lose the notion that it's the opposition that matters
(InputMethod::InactiveSince(_old), InputMethod::Active(new_im))
=> Self {
im: InputMethod::Active(new_im),
visibility_override: visibility::State::NotForced,
..self
},
(InputMethod::Active(_old), InputMethod::InactiveSince(since))
=> Self {
im: InputMethod::InactiveSince(since),
visibility_override: visibility::State::NotForced,
..self
},
// This is a weird case, there's no need to update an inactive state.
// But it's not wrong, just superfluous.
(InputMethod::InactiveSince(old), InputMethod::InactiveSince(_new))
=> Self {
// New is going to be newer than old, so it can be ignored.
// It was already inactive at that moment.
im: InputMethod::InactiveSince(old),
..self
},
}
}
}
pub fn get_outcome(&self, now: Instant) -> Outcome {
// FIXME: include physical keyboard presence
Outcome {
visibility: match (self.physical_keyboard, self.visibility_override) {
(_, visibility::State::ForcedHidden) => animation::Outcome::Hidden,
(_, visibility::State::ForcedVisible) => animation::Outcome::Visible,
(Presence::Present, visibility::State::NotForced) => animation::Outcome::Hidden,
(Presence::Missing, visibility::State::NotForced) => match self.im {
InputMethod::Active(_) => animation::Outcome::Visible,
InputMethod::InactiveSince(since) => {
if now < since + animation::HIDING_TIMEOUT { animation::Outcome::Visible }
else { animation::Outcome::Hidden }
},
},
},
im: self.im.clone(),
}
}
/// Returns the next time to update the outcome.
pub fn get_next_wake(&self, now: Instant) -> Option<Instant> {
match self {
Self {
visibility_override: visibility::State::NotForced,
im: InputMethod::InactiveSince(since),
..
} => {
let anim_end = *since + animation::HIDING_TIMEOUT;
if now < anim_end { Some(anim_end) }
else { None }
}
_ => None,
}
}
}
#[cfg(test)]
mod test {
use super::*;
use std::time::Duration;
fn imdetails_new() -> InputMethodDetails {
InputMethodDetails {
purpose: ContentPurpose::Normal,
hint: ContentHint::NONE,
}
}
/// Test the original delay scenario: no flicker on quick switches.
#[test]
fn avoid_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 state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);
// Check 100ms at 1ms intervals. It should remain visible.
for _i in 0..100 {
now += Duration::from_millis(1);
assert_eq!(
state.get_outcome(now).visibility,
animation::Outcome::Visible,
"Hidden when it should remain visible: {:?}",
now.saturating_duration_since(start),
)
}
let state = state.apply_event(Event::InputMethod(InputMethod::Active(imdetails_new())), now);
assert_eq!(state.get_outcome(now).visibility, animation::Outcome::Visible);
}
/// Make sure that hiding works when input method goes away
#[test]
fn 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 state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);
while let animation::Outcome::Visible = state.get_outcome(now).visibility {
now += Duration::from_millis(1);
assert!(
now < start + Duration::from_millis(250),
"Hiding too slow: {:?}",
now.saturating_duration_since(start),
);
}
}
/// Check against the false showing bug.
/// Expectation: it will get hidden and not appear again
#[test]
fn false_show() {
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,
};
// This reflects the sequence from Wayland:
// disable, disable, enable, disable
// all in a single batch.
let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);
let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);
let state = state.apply_event(Event::InputMethod(InputMethod::Active(imdetails_new())), now);
let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);
while let animation::Outcome::Visible = state.get_outcome(now).visibility {
now += Duration::from_millis(1);
assert!(
now < start + Duration::from_millis(250),
"Still not hidden: {:?}",
now.saturating_duration_since(start),
);
}
// One second without appearing again
for _i in 0..1000 {
now += Duration::from_millis(1);
assert_eq!(
state.get_outcome(now).visibility,
animation::Outcome::Hidden,
"Appeared unnecessarily: {:?}",
now.saturating_duration_since(start),
);
}
}
#[test]
fn force_visible() {
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::InactiveSince(now),
physical_keyboard: Presence::Missing,
visibility_override: visibility::State::NotForced,
};
now += Duration::from_secs(1);
let state = state.apply_event(Event::Visibility(visibility::Event::ForceVisible), now);
assert_eq!(
state.get_outcome(now).visibility,
animation::Outcome::Visible,
"Failed to show: {:?}",
now.saturating_duration_since(start),
);
now += Duration::from_secs(1);
let state = state.apply_event(Event::InputMethod(InputMethod::Active(imdetails_new())), now);
now += Duration::from_secs(1);
let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);
now += Duration::from_secs(1);
assert_eq!(
state.get_outcome(now).visibility,
animation::Outcome::Hidden,
"Failed to release forced visibility: {:?}",
now.saturating_duration_since(start),
);
}
#[test]
fn keyboard_present() {
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,
};
now += Duration::from_secs(1);
let state = state.apply_event(Event::PhysicalKeyboard(Presence::Present), now);
assert_eq!(
state.get_outcome(now).visibility,
animation::Outcome::Hidden,
"Failed to hide: {:?}",
now.saturating_duration_since(start),
);
now += Duration::from_secs(1);
let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);
now += Duration::from_secs(1);
let state = state.apply_event(Event::InputMethod(InputMethod::Active(imdetails_new())), now);
assert_eq!(
state.get_outcome(now).visibility,
animation::Outcome::Hidden,
"Failed to remain hidden: {:?}",
now.saturating_duration_since(start),
);
now += Duration::from_secs(1);
let state = state.apply_event(Event::PhysicalKeyboard(Presence::Missing), now);
assert_eq!(
state.get_outcome(now).visibility,
animation::Outcome::Visible,
"Failed to appear: {:?}",
now.saturating_duration_since(start),
);
}
}

View File

@ -4,20 +4,12 @@
#include "input-method-unstable-v2-client-protocol.h" #include "input-method-unstable-v2-client-protocol.h"
#include "virtual-keyboard-unstable-v1-client-protocol.h" #include "virtual-keyboard-unstable-v1-client-protocol.h"
#include "eek/eek-types.h" #include "eek/eek-types.h"
#include "main.h"
#include "src/ui_manager.h" #include "src/ui_manager.h"
struct submission;
struct squeek_layout; struct squeek_layout;
struct submission* get_submission(struct zwp_input_method_manager_v2 *immanager,
struct zwp_virtual_keyboard_manager_v1 *vkmanager,
struct vis_manager *vis_manager,
struct wl_seat *seat,
EekboardContextService *state);
// Defined in Rust // Defined in Rust
struct submission* submission_new(struct zwp_input_method_v2 *im, struct zwp_virtual_keyboard_v1 *vk, EekboardContextService *state, struct vis_manager *vis_manager);
uint8_t submission_hint_available(struct submission *self); uint8_t submission_hint_available(struct submission *self);
void submission_set_ui(struct submission *self, ServerContextService *ui_context);
void submission_use_layout(struct submission *self, struct squeek_layout *layout, uint32_t time); void submission_use_layout(struct submission *self, struct squeek_layout *layout, uint32_t time);
#endif #endif

View File

@ -19,12 +19,13 @@
use std::collections::HashSet; use std::collections::HashSet;
use std::ffi::CString; use std::ffi::CString;
use crate::vkeyboard::c::ZwpVirtualKeyboardV1;
use ::action::Modifier; use ::action::Modifier;
use ::imservice; use ::imservice;
use ::imservice::IMService; use ::imservice::IMService;
use ::keyboard::{ KeyCode, KeyStateId, Modifiers, PressType }; use ::keyboard::{ KeyCode, KeyStateId, Modifiers, PressType };
use ::layout; use ::layout;
use ::ui_manager::VisibilityManager;
use ::util::vec_remove; use ::util::vec_remove;
use ::vkeyboard; use ::vkeyboard;
use ::vkeyboard::VirtualKeyboard; use ::vkeyboard::VirtualKeyboard;
@ -36,50 +37,10 @@ use std::iter::FromIterator;
pub mod c { pub mod c {
use super::*; use super::*;
use std::os::raw::c_void; use crate::util::c::Wrapped;
use ::imservice::c::InputMethod;
use ::util::c::Wrapped;
use ::vkeyboard::c::ZwpVirtualKeyboardV1;
// The following defined in C
/// EekboardContextService*
#[repr(transparent)]
pub struct StateManager(*const c_void);
pub type Submission = Wrapped<super::Submission>; pub type Submission = Wrapped<super::Submission>;
#[no_mangle]
pub extern "C"
fn submission_new(
im: *mut InputMethod,
vk: ZwpVirtualKeyboardV1,
state_manager: *const StateManager,
visibility_manager: Wrapped<VisibilityManager>,
) -> Submission {
let imservice = if im.is_null() {
None
} else {
let visibility_manager = visibility_manager.clone_ref();
Some(IMService::new(
im,
state_manager,
Box::new(move |active| visibility_manager.borrow_mut().set_im_active(active)),
))
};
// TODO: add vkeyboard too
Wrapped::new(
super::Submission {
imservice,
modifiers_active: Vec::new(),
virtual_keyboard: VirtualKeyboard(vk),
pressed: Vec::new(),
keymap_fds: Vec::new(),
keymap_idx: None,
}
)
}
#[no_mangle] #[no_mangle]
pub extern "C" pub extern "C"
@ -131,6 +92,17 @@ pub enum SubmitData<'a> {
} }
impl Submission { impl Submission {
pub fn new(vk: ZwpVirtualKeyboardV1, imservice: Option<Box<IMService>>) -> Self {
Submission {
imservice,
modifiers_active: Vec::new(),
virtual_keyboard: VirtualKeyboard(vk),
pressed: Vec::new(),
keymap_fds: Vec::new(),
keymap_idx: None,
}
}
/// Sends a submit text event if possible; /// Sends a submit text event if possible;
/// otherwise sends key press and makes a note of it /// otherwise sends key press and makes a note of it
pub fn handle_press( pub fn handle_press(

View File

@ -3,9 +3,9 @@
#include <inttypes.h> #include <inttypes.h>
#include "animation.h"
#include "eek/eek-types.h" #include "eek/eek-types.h"
#include "outputs.h" #include "outputs.h"
#include "main.h"
struct ui_manager; struct ui_manager;
@ -15,6 +15,5 @@ uint32_t squeek_uiman_get_perceptual_height(struct ui_manager *uiman);
struct vis_manager; struct vis_manager;
struct vis_manager *squeek_visman_new(struct squeek_animation_visibility_manager *animman); struct vis_manager *squeek_visman_new(struct squeek_state_manager *state_manager);
void squeek_visman_set_keyboard_present(struct vis_manager *visman, uint32_t keyboard_present);
#endif #endif

View File

@ -1,54 +1,20 @@
/* Copyright (C) 2020 Purism SPC /* Copyright (C) 2020, 2021 Purism SPC
* SPDX-License-Identifier: GPL-3.0+ * SPDX-License-Identifier: GPL-3.0+
*/ */
/*! Centrally manages the shape of the UI widgets, and the choice of layout. /*! Centrally manages the shape of the UI widgets, and the choice of layout.
* *
* Coordinates this based on information collated from all possible sources. * Coordinates this based on information collated from all possible sources.
*
* Somewhat obsoleted by the `animation` module
* (except keyboard presence calculation),
* and could be folded into that tracker loop as another piece of state.
*/ */
use crate::animation::{
ThreadLoopDriver as Receiver,
Event as ReceiverMessage,
};
use crate::logging;
use std::cmp::min; use std::cmp::min;
use ::outputs::c::OutputHandle; use ::outputs::c::OutputHandle;
use crate::logging::Warn;
pub mod c { pub mod c {
use super::*; use super::*;
use ::util::c::Wrapped; use ::util::c::Wrapped;
use ::util::CloneOwned;
#[no_mangle]
pub extern "C"
fn squeek_visman_new(receiver: Wrapped<Receiver>) -> Wrapped<VisibilityManager> {
Wrapped::new(VisibilityManager {
receiver: receiver.clone_owned(),
visibility_state: VisibilityFactors {
im_active: false,
physical_keyboard_present: false,
}
})
}
#[no_mangle]
pub extern "C"
fn squeek_visman_set_keyboard_present(visman: Wrapped<VisibilityManager>, present: u32) {
let visman = visman.clone_ref();
let mut visman = visman.borrow_mut();
visman.set_keyboard_present(present != 0)
}
#[no_mangle] #[no_mangle]
pub extern "C" pub extern "C"
fn squeek_uiman_new() -> Wrapped<Manager> { fn squeek_uiman_new() -> Wrapped<Manager> {
@ -114,106 +80,3 @@ impl Manager {
} }
} }
} }
#[derive(PartialEq, Debug)]
enum Visibility {
Hidden,
Visible,
}
#[derive(Debug)]
enum VisibilityTransition {
/// Hide immediately
Hide,
/// Hide if no show request comes soon
Release,
/// Show instantly
Show,
/// Don't do anything
NoTransition,
}
/// Contains visibility policy
#[derive(Clone, Debug)]
struct VisibilityFactors {
im_active: bool,
physical_keyboard_present: bool,
}
impl VisibilityFactors {
/// Static policy.
/// Use when transitioning from an undefined state (e.g. no UI before).
fn desired(&self) -> Visibility {
match self {
VisibilityFactors {
im_active: true,
physical_keyboard_present: false,
} => Visibility::Visible,
_ => Visibility::Hidden,
}
}
/// Stateful policy
fn transition_to(&self, next: &Self) -> VisibilityTransition {
use self::Visibility::*;
let im_deactivation = self.im_active && !next.im_active;
match (self.desired(), next.desired(), im_deactivation) {
(Visible, Hidden, true) => VisibilityTransition::Release,
(Visible, Hidden, _) => VisibilityTransition::Hide,
(Hidden, Visible, _) => VisibilityTransition::Show,
_ => VisibilityTransition::NoTransition,
}
}
}
pub struct VisibilityManager {
/// Forward changes there.
receiver: Receiver,
visibility_state: VisibilityFactors,
}
impl VisibilityManager {
fn apply_changes(&mut self, new: Self) {
let request = match self.visibility_state.transition_to(&new.visibility_state) {
VisibilityTransition::Hide => Some(ReceiverMessage::ForceHide),
VisibilityTransition::Show => Some(ReceiverMessage::ClaimVisible),
VisibilityTransition::Release => Some(ReceiverMessage::ReleaseVisible),
VisibilityTransition::NoTransition => None,
};
if let Some(request) = request {
new.receiver.send(request)
.or_warn(&mut logging::Print, logging::Problem::Warning, "Can't send to animation manager");
}
*self = new;
}
pub fn set_im_active(&mut self, im_active: bool) {
let new = VisibilityManager {
visibility_state: VisibilityFactors {
im_active,
..self.visibility_state.clone()
},
..unsafe { self.clone() }
};
self.apply_changes(new);
}
pub fn set_keyboard_present(&mut self, keyboard_present: bool) {
let new = VisibilityManager {
visibility_state: VisibilityFactors {
physical_keyboard_present: keyboard_present,
..self.visibility_state.clone()
},
..unsafe { self.clone() }
};
self.apply_changes(new);
}
/// This is only a helper for getting desired visibility.
unsafe fn clone(&self) -> Self {
VisibilityManager {
receiver: self.receiver.clone(),
visibility_state: self.visibility_state.clone(),
}
}
}