diff --git a/src/animation.h b/src/animation.h deleted file mode 100644 index 31c4c98d..00000000 --- a/src/animation.h +++ /dev/null @@ -1,13 +0,0 @@ -#pragma once -#include - -// 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); diff --git a/src/animation.rs b/src/animation.rs index 0d455644..7ea07dfa 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -2,441 +2,16 @@ * SPDX-License-Identifier: GPL-3.0+ */ -/*! Animation state trackers and drivers. - * 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; +/*! Animation details */ +use std::time::Duration; /// The keyboard should hide after this has elapsed to prevent flickering. -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), -} +pub const HIDING_TIMEOUT: Duration = Duration::from_millis(200); /// The outwardly visible state of visibility -#[derive(PartialEq, Debug)] +#[derive(PartialEq, Debug, Clone)] pub enum Outcome { Visible, 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 { - 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, -} - -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) { - // 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; -type UISender = glib::Sender; - -/// 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> { - 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) - -> Wrapped - { - 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) { - 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) { - 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); - } -} diff --git a/src/dbus.c b/src/dbus.c index 14cc32d0..b894a2e1 100644 --- a/src/dbus.c +++ b/src/dbus.c @@ -19,7 +19,9 @@ #include "config.h" #include "dbus.h" +#include "main.h" +#include #include #include @@ -53,9 +55,9 @@ handle_set_visible(SmPuriOSK0 *object, GDBusMethodInvocation *invocation, DBusHandler *service = user_data; if (arg_visible) { - squeek_animation_visibility_manager_send_claim_visible (service->animman); + squeek_state_send_force_visible (service->state_manager); } 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); @@ -65,12 +67,12 @@ handle_set_visible(SmPuriOSK0 *object, GDBusMethodInvocation *invocation, DBusHandler * dbus_handler_new (GDBusConnection *connection, const gchar *object_path, - struct squeek_animation_visibility_manager *animman) + struct squeek_state_manager *state_manager) { DBusHandler *self = calloc(1, sizeof(DBusHandler)); self->object_path = g_strdup(object_path); self->connection = connection; - self->animman = animman; + self->state_manager = state_manager; self->dbus_interface = sm_puri_osk0_skeleton_new(); g_signal_connect(self->dbus_interface, "handle-set-visible", diff --git a/src/dbus.h b/src/dbus.h index 49e2e46d..9080bf28 100644 --- a/src/dbus.h +++ b/src/dbus.h @@ -19,10 +19,11 @@ #ifndef DBUS_H_ #define DBUS_H_ 1 -#include "animation.h" - #include "sm.puri.OSK0.h" +// From main.h +struct squeek_state_manager; + G_BEGIN_DECLS #define DBUS_SERVICE_PATH "/sm/puri/OSK0" @@ -41,12 +42,12 @@ typedef struct _DBusHandler char *object_path; /// Forward incoming events there - struct squeek_animation_visibility_manager *animman; // shared reference + struct squeek_state_manager *state_manager; // shared reference } DBusHandler; DBusHandler * dbus_handler_new (GDBusConnection *connection, const gchar *object_path, - struct squeek_animation_visibility_manager *animman); + struct squeek_state_manager *state_manager); void dbus_handler_destroy(DBusHandler*); G_END_DECLS diff --git a/src/event_loop/driver.rs b/src/event_loop/driver.rs new file mode 100644 index 00000000..62ab185f --- /dev/null +++ b/src/event_loop/driver.rs @@ -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; +type UISender = glib::Sender; + +/// 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> { + 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) { + 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) { + 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, 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"); + } +} diff --git a/src/event_loop/mod.rs b/src/event_loop/mod.rs new file mode 100644 index 00000000..fa749aab --- /dev/null +++ b/src/event_loop/mod.rs @@ -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, + 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); + } +} diff --git a/src/imservice.c b/src/imservice.c index 25dc65e4..3797941e 100644 --- a/src/imservice.c +++ b/src/imservice.c @@ -23,22 +23,6 @@ static const struct zwp_input_method_v2_listener input_method_listener = { .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 struct zwp_input_method_v2 *imservice_manager_get_input_method(struct zwp_input_method_manager_v2 *manager, struct wl_seat *seat) { diff --git a/src/imservice.rs b/src/imservice.rs index f7974a9a..1ad7621c 100644 --- a/src/imservice.rs +++ b/src/imservice.rs @@ -8,7 +8,11 @@ use std::ffi::CString; use std::fmt; use std::num::Wrapping; use std::string::String; +use std::time::Instant; +use crate::event_loop::driver; +use crate::state; +use crate::state::Event; use ::logging; use ::util::c::into_cstring; @@ -23,8 +27,6 @@ pub mod c { use std::os::raw::{c_char, c_void}; - pub use ::submission::c::StateManager; - // The following defined in C /// 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_delete_surrounding_text(im: *mut InputMethod, before: u32, after: 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 @@ -140,7 +141,6 @@ pub mod c { im: *const InputMethod) { let imservice = check_imservice(imservice, im).unwrap(); - let active_changed = imservice.current.active ^ imservice.pending.active; imservice.current = imservice.pending.clone(); imservice.pending = IMProtocolState { @@ -149,19 +149,7 @@ pub mod c { }; imservice.serial += Wrapping(1u32); - - 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, - ); - } - } - } + imservice.send_event(); } // TODO: this is really untested @@ -177,7 +165,7 @@ pub mod c { // the keyboard is already decommissioned imservice.current.active = false; - (imservice.active_callback)(imservice.current.active); + imservice.send_event(); } // FIXME: destroy and deallocate @@ -328,9 +316,7 @@ impl Default for IMProtocolState { pub struct IMService { /// Owned reference (still created and destroyed in C) pub im: *mut c::InputMethod, - /// Unowned reference. Be careful, it's shared with C at large - state_manager: *const c::StateManager, - active_callback: Box, + sender: driver::Threaded, pending: IMProtocolState, current: IMProtocolState, // turn current into an idiomatic representation? @@ -346,15 +332,13 @@ pub enum SubmitError { impl IMService { pub fn new( im: *mut c::InputMethod, - state_manager: *const c::StateManager, - active_callback: Box, + sender: driver::Threaded, ) -> Box { // IMService will be referenced to by C, // so it needs to stay in the same place in memory via Box let imservice = Box::new(IMService { im, - active_callback, - state_manager, + sender, pending: IMProtocolState::default(), current: IMProtocolState::default(), preedit_string: String::new(), @@ -414,4 +398,21 @@ impl IMService { pub fn is_active(&self) -> bool { 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"); + } } diff --git a/src/lib.rs b/src/lib.rs index 1478ccbf..05cab4dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,7 @@ mod action; mod animation; pub mod data; mod drawing; +mod event_loop; pub mod float_ord; pub mod imservice; mod keyboard; @@ -32,6 +33,7 @@ mod manager; mod outputs; mod popover; mod resources; +mod state; mod style; mod submission; pub mod tests; diff --git a/src/main.h b/src/main.h index ea0d5a03..c2c62a77 100644 --- a/src/main.h +++ b/src/main.h @@ -1,17 +1,33 @@ #pragma once /// This all wraps https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/struct.MainContext.html#method.channel +#include + +#include "input-method-unstable-v2-client-protocol.h" +#include "virtual-keyboard-unstable-v1-client-protocol.h" + #include "eek/eek-types.h" #include "dbus.h" -struct receiver; -struct sender; -struct channel { - struct sender *sender; +struct receiver; + +/// Wrapped +struct squeek_state_manager; + +struct submission; + +struct rsobjects { 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); + +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); diff --git a/src/main.rs b/src/main.rs index daf1b5b2..0a089bf1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,17 +4,23 @@ /*! Glue for the main loop. */ -use crate::animation::Outcome as Message; -use glib::{Continue, MainContext, PRIORITY_DEFAULT, Receiver, Sender}; -use std::thread; -use std::time::Duration; +use crate::state; +use glib::{Continue, MainContext, PRIORITY_DEFAULT, Receiver}; + mod c { use super::*; use std::os::raw::c_void; 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* #[repr(transparent)] @@ -24,56 +30,56 @@ mod c { #[repr(transparent)] pub struct DBusHandler(*const c_void); - /// Corresponds to main.c::channel + /// Holds the Rust structures that are interesting from C. #[repr(C)] - pub struct Channel { - sender: ArcWrapped>, - receiver: Wrapped>, + pub struct RsObjects { + receiver: Wrapped>, + state_manager: Wrapped, + submission: Wrapped, } extern "C" { - pub fn server_context_service_real_show_keyboard(imservice: *const UIManager); - pub fn server_context_service_real_hide_keyboard(imservice: *const UIManager); + fn server_context_service_real_show_keyboard(service: *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, // 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] 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 = ArcWrapped::new(sender); - let receiver = Wrapped::new(receiver); - let channel = Channel { - sender, - receiver, + + let now = Instant::now(); + let state_manager = driver::Threaded::new(sender, state::Application::new(now)); + + 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()); - - channel - } - - /// testing only - fn start_work(sender: ArcWrapped>) { - 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(); - }); + RsObjects { + submission: Wrapped::new(submission), + state_manager: Wrapped::new(state_manager), + receiver: Wrapped::new(receiver), + } } /// Places the UI loop callback in the glib main loop. #[no_mangle] pub extern "C" fn register_ui_loop_handler( - receiver: Wrapped>, + receiver: Wrapped>, ui_manager: *const UIManager, dbus_handler: *const DBusHandler, ) { @@ -97,23 +103,47 @@ mod c { /// This is the outest layer of the imperative shell, /// and doesn't lend itself to testing other than integration. fn main_loop_handle_message( - msg: Message, + msg: Commands, ui_manager: *const UIManager, dbus_handler: *const DBusHandler, ) { - match msg { - Message::Visible => 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 + match msg.panel_visibility { + Some(PanelCommand::Show) => unsafe { 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); - 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, + pub layout_hint_set: Option, + pub dbus_visible_set: Option, +} diff --git a/src/server-context-service.c b/src/server-context-service.c index 77cb66dc..92bc3a10 100644 --- a/src/server-context-service.c +++ b/src/server-context-service.c @@ -42,7 +42,7 @@ struct _ServerContextService { struct submission *submission; // unowned struct squeek_layout_state *layout; struct ui_manager *manager; // unowned - struct vis_manager *vis_manager; // owned + struct squeek_state_manager *state_manager; // shared reference PhoshLayerSurface *window; GtkWidget *widget; // nullable @@ -203,6 +203,7 @@ make_widget (ServerContextService *self) gtk_widget_show_all(self->widget); } +// Called from rust void 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)); } +// Called from rust void server_context_service_real_hide_keyboard (ServerContextService *self) { - 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); + if (self->window) { + gtk_widget_hide (GTK_WIDGET(self->window)); + } } static void @@ -238,7 +235,7 @@ server_context_service_set_property (GObject *object, switch (prop_id) { 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; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); @@ -321,14 +318,20 @@ init (ServerContextService *self) { } 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); ui->submission = submission; ui->state = self; ui->layout = layout; ui->manager = uiman; - ui->vis_manager = visman; + ui->state_manager = state_manager; init(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); +} diff --git a/src/server-context-service.h b/src/server-context-service.h index a77a8edd..2cecd9b6 100644 --- a/src/server-context-service.h +++ b/src/server-context-service.h @@ -29,7 +29,7 @@ G_BEGIN_DECLS /** Manages the lifecycle of the window displaying layouts. */ 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 *); void server_context_service_force_show_keyboard (ServerContextService *self); void server_context_service_hide_keyboard (ServerContextService *self); diff --git a/src/server-main.c b/src/server-main.c index 18c016a2..1799ccfa 100644 --- a/src/server-main.c +++ b/src/server-main.c @@ -25,7 +25,6 @@ #include "config.h" -#include "animation.h" #include "eek/eek.h" #include "eekboard/eekboard-context-service.h" #include "dbus.h" @@ -47,15 +46,18 @@ typedef enum _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 squeek_wayland wayland; // Just hooks. DBusHandler *dbus_handler; // Controls visibility of the OSK. EekboardContextService *settings_context; // Gsettings hooks. ServerContextService *ui_context; // mess, includes the entire UI - struct submission *submission; // Wayland text input handling. - struct squeek_layout_state layout_choice; // Currently wanted layout. - struct ui_manager *ui_manager; // UI shape tracker/chooser. TODO: merge with layuot choice + /// Currently wanted layout. TODO: merge into state::Application + struct squeek_layout_state layout_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); } +/// 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[] = { @@ -377,10 +394,9 @@ main (int argc, char **argv) g_warning("Wayland input method interface not available"); } - - struct channel ui_channel = main_loop_channel_new(); - - struct squeek_animation_visibility_manager *animman = squeek_animation_visibility_manager_new(ui_channel.sender); + struct rsobjects rsobjects = create_rsobjects(instance.wayland.input_method_manager, + instance.wayland.virtual_keyboard_manager, + instance.wayland.seat); instance.ui_manager = squeek_uiman_new(); @@ -401,7 +417,7 @@ main (int argc, char **argv) guint owner_id = 0; DBusHandler *service = NULL; 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) { 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); - - 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); + eekboard_context_service_set_submission(instance.settings_context, rsobjects.submission); ServerContextService *ui_context = server_context_service_new( instance.settings_context, - instance.submission, + rsobjects.submission, &instance.layout_choice, instance.ui_manager, - vis_manager); + rsobjects.state_manager); if (!ui_context) { g_error("Could not initialize GUI"); exit(1); } 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(); - if (debug_flags & SQUEEKBOARD_DEBUG_FLAG_FORCE_SHOW) { - server_context_service_force_show_keyboard (ui_context); - } if (debug_flags & SQUEEKBOARD_DEBUG_FLAG_GTK_INSPECTOR) { 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); g_main_loop_run (loop); diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 00000000..24b89d94 --- /dev/null +++ b/src/state.rs @@ -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 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 { + 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), + ); + + } +} diff --git a/src/submission.h b/src/submission.h index 1e7274ae..ed1093f8 100644 --- a/src/submission.h +++ b/src/submission.h @@ -4,20 +4,12 @@ #include "input-method-unstable-v2-client-protocol.h" #include "virtual-keyboard-unstable-v1-client-protocol.h" #include "eek/eek-types.h" +#include "main.h" #include "src/ui_manager.h" -struct submission; 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 -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); -void submission_set_ui(struct submission *self, ServerContextService *ui_context); void submission_use_layout(struct submission *self, struct squeek_layout *layout, uint32_t time); #endif diff --git a/src/submission.rs b/src/submission.rs index 59212ba7..39da68f1 100644 --- a/src/submission.rs +++ b/src/submission.rs @@ -19,12 +19,13 @@ use std::collections::HashSet; use std::ffi::CString; + +use crate::vkeyboard::c::ZwpVirtualKeyboardV1; use ::action::Modifier; use ::imservice; use ::imservice::IMService; use ::keyboard::{ KeyCode, KeyStateId, Modifiers, PressType }; use ::layout; -use ::ui_manager::VisibilityManager; use ::util::vec_remove; use ::vkeyboard; use ::vkeyboard::VirtualKeyboard; @@ -35,51 +36,11 @@ use std::iter::FromIterator; /// Gathers stuff defined in C or called by C pub mod c { use super::*; - - use std::os::raw::c_void; - 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); + use crate::util::c::Wrapped; pub type Submission = Wrapped; - #[no_mangle] - pub extern "C" - fn submission_new( - im: *mut InputMethod, - vk: ZwpVirtualKeyboardV1, - state_manager: *const StateManager, - visibility_manager: Wrapped, - ) -> 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] pub extern "C" @@ -131,6 +92,17 @@ pub enum SubmitData<'a> { } impl Submission { + pub fn new(vk: ZwpVirtualKeyboardV1, imservice: Option>) -> 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; /// otherwise sends key press and makes a note of it pub fn handle_press( diff --git a/src/ui_manager.h b/src/ui_manager.h index ff3bab3a..b5f1734e 100644 --- a/src/ui_manager.h +++ b/src/ui_manager.h @@ -3,9 +3,9 @@ #include -#include "animation.h" #include "eek/eek-types.h" #include "outputs.h" +#include "main.h" struct ui_manager; @@ -15,6 +15,5 @@ uint32_t squeek_uiman_get_perceptual_height(struct ui_manager *uiman); struct vis_manager; -struct vis_manager *squeek_visman_new(struct squeek_animation_visibility_manager *animman); -void squeek_visman_set_keyboard_present(struct vis_manager *visman, uint32_t keyboard_present); +struct vis_manager *squeek_visman_new(struct squeek_state_manager *state_manager); #endif diff --git a/src/ui_manager.rs b/src/ui_manager.rs index a4c30386..b189a479 100644 --- a/src/ui_manager.rs +++ b/src/ui_manager.rs @@ -1,54 +1,20 @@ -/* Copyright (C) 2020 Purism SPC +/* Copyright (C) 2020, 2021 Purism SPC * SPDX-License-Identifier: GPL-3.0+ */ /*! Centrally manages the shape of the UI widgets, and the choice of layout. * * 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 ::outputs::c::OutputHandle; -use crate::logging::Warn; - - pub mod c { use super::*; use ::util::c::Wrapped; - use ::util::CloneOwned; - - #[no_mangle] - pub extern "C" - fn squeek_visman_new(receiver: Wrapped) -> Wrapped { - 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, present: u32) { - let visman = visman.clone_ref(); - let mut visman = visman.borrow_mut(); - visman.set_keyboard_present(present != 0) - } - #[no_mangle] pub extern "C" fn squeek_uiman_new() -> Wrapped { @@ -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(), - } - } -}