Merge branch 'animation' into 'master'
visibility state machine See merge request World/Phosh/squeekboard!494
This commit is contained in:
17
src/animation.rs
Normal file
17
src/animation.rs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/* Copyright (C) 2020 Purism SPC
|
||||||
|
* SPDX-License-Identifier: GPL-3.0+
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*! Animation details */
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// The keyboard should hide after this has elapsed to prevent flickering.
|
||||||
|
pub const HIDING_TIMEOUT: Duration = Duration::from_millis(200);
|
||||||
|
|
||||||
|
/// The outwardly visible state of visibility
|
||||||
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
|
pub enum Outcome {
|
||||||
|
Visible,
|
||||||
|
Hidden,
|
||||||
|
}
|
||||||
51
src/dbus.c
51
src/dbus.c
@ -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>
|
||||||
|
|
||||||
@ -44,11 +46,6 @@ dbus_handler_destroy(DBusHandler *service)
|
|||||||
service->introspection_data = NULL;
|
service->introspection_data = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (service->context) {
|
|
||||||
g_signal_handlers_disconnect_by_data (service->context, service);
|
|
||||||
service->context = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
free(service);
|
free(service);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,38 +54,25 @@ handle_set_visible(SmPuriOSK0 *object, GDBusMethodInvocation *invocation,
|
|||||||
gboolean arg_visible, gpointer user_data) {
|
gboolean arg_visible, gpointer user_data) {
|
||||||
DBusHandler *service = user_data;
|
DBusHandler *service = user_data;
|
||||||
|
|
||||||
if (service->context) {
|
if (arg_visible) {
|
||||||
if (arg_visible) {
|
squeek_state_send_force_visible (service->state_manager);
|
||||||
server_context_service_force_show_keyboard (service->context);
|
} else {
|
||||||
} else {
|
squeek_state_send_force_hidden(service->state_manager);
|
||||||
server_context_service_hide_keyboard (service->context);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sm_puri_osk0_complete_set_visible(object, invocation);
|
sm_puri_osk0_complete_set_visible(object, invocation);
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void on_visible(DBusHandler *service,
|
|
||||||
GParamSpec *pspec,
|
|
||||||
ServerContextService *context)
|
|
||||||
{
|
|
||||||
(void)pspec;
|
|
||||||
gboolean visible;
|
|
||||||
|
|
||||||
g_return_if_fail (SERVER_IS_CONTEXT_SERVICE (context));
|
|
||||||
|
|
||||||
g_object_get (context, "visible", &visible, NULL);
|
|
||||||
|
|
||||||
sm_puri_osk0_set_visible(service->dbus_interface, visible);
|
|
||||||
}
|
|
||||||
|
|
||||||
DBusHandler *
|
DBusHandler *
|
||||||
dbus_handler_new (GDBusConnection *connection,
|
dbus_handler_new (GDBusConnection *connection,
|
||||||
const gchar *object_path)
|
const gchar *object_path,
|
||||||
|
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->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",
|
||||||
@ -109,16 +93,9 @@ dbus_handler_new (GDBusConnection *connection,
|
|||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
// Exported to Rust
|
||||||
dbus_handler_set_ui_context(DBusHandler *service,
|
void dbus_handler_set_visible(DBusHandler *service,
|
||||||
ServerContextService *context)
|
uint8_t visible)
|
||||||
{
|
{
|
||||||
g_return_if_fail (!service->context);
|
sm_puri_osk0_set_visible(service->dbus_interface, visible);
|
||||||
|
|
||||||
service->context = context;
|
|
||||||
|
|
||||||
g_signal_connect_swapped (service->context,
|
|
||||||
"notify::visible",
|
|
||||||
G_CALLBACK(on_visible),
|
|
||||||
service);
|
|
||||||
}
|
}
|
||||||
|
|||||||
18
src/dbus.h
18
src/dbus.h
@ -19,15 +19,20 @@
|
|||||||
#ifndef DBUS_H_
|
#ifndef DBUS_H_
|
||||||
#define DBUS_H_ 1
|
#define DBUS_H_ 1
|
||||||
|
|
||||||
#include "server-context-service.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"
|
||||||
#define DBUS_SERVICE_INTERFACE "sm.puri.OSK0"
|
#define DBUS_SERVICE_INTERFACE "sm.puri.OSK0"
|
||||||
|
|
||||||
|
/// Two jobs: accept events, forwarding them to the visibility manager,
|
||||||
|
/// and get updated from inside to show internal state.
|
||||||
|
/// Updates are handled in the same loop as the UI.
|
||||||
|
/// See main.rs
|
||||||
typedef struct _DBusHandler
|
typedef struct _DBusHandler
|
||||||
{
|
{
|
||||||
GDBusConnection *connection;
|
GDBusConnection *connection;
|
||||||
@ -36,13 +41,14 @@ typedef struct _DBusHandler
|
|||||||
guint registration_id;
|
guint registration_id;
|
||||||
char *object_path;
|
char *object_path;
|
||||||
|
|
||||||
ServerContextService *context; // unowned reference
|
/// Forward incoming events there
|
||||||
|
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,
|
||||||
void dbus_handler_set_ui_context(DBusHandler *service,
|
struct squeek_state_manager *state_manager);
|
||||||
ServerContextService *context);
|
|
||||||
void dbus_handler_destroy(DBusHandler*);
|
void dbus_handler_destroy(DBusHandler*);
|
||||||
G_END_DECLS
|
G_END_DECLS
|
||||||
#endif /* DBUS_H_ */
|
#endif /* DBUS_H_ */
|
||||||
|
|||||||
141
src/event_loop/driver.rs
Normal file
141
src/event_loop/driver.rs
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
/* Copyright (C) 2021 Purism SPC
|
||||||
|
* SPDX-License-Identifier: GPL-3.0+
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*! This drives the loop from the `loop` module.
|
||||||
|
*
|
||||||
|
* The tracker loop needs to be driven somehow,
|
||||||
|
* and connected to the external world,
|
||||||
|
* both on the side of receiving and sending events.
|
||||||
|
*
|
||||||
|
* That's going to be implementation-dependent,
|
||||||
|
* connecting to some external mechanisms
|
||||||
|
* for time, messages, and threading/callbacks.
|
||||||
|
*
|
||||||
|
* This is the "imperative shell" part of the software,
|
||||||
|
* and no longer unit-testable.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::event_loop;
|
||||||
|
use crate::logging;
|
||||||
|
use crate::main::Commands;
|
||||||
|
use crate::state::{ Application, Event };
|
||||||
|
use glib;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
// Traits
|
||||||
|
use crate::logging::Warn;
|
||||||
|
|
||||||
|
|
||||||
|
type Sender = mpsc::Sender<Event>;
|
||||||
|
type UISender = glib::Sender<Commands>;
|
||||||
|
|
||||||
|
/// This loop driver spawns a new thread which updates the state in a loop,
|
||||||
|
/// in response to incoming events.
|
||||||
|
/// It sends outcomes to the glib main loop using a channel.
|
||||||
|
/// The outcomes are applied by the UI end of the channel in the `main` module.
|
||||||
|
// This could still be reasonably tested,
|
||||||
|
// by creating a glib::Sender and checking what messages it receives.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Threaded {
|
||||||
|
thread: Sender,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Threaded {
|
||||||
|
pub fn new(ui: UISender, initial_state: Application) -> Self {
|
||||||
|
let (sender, receiver) = mpsc::channel();
|
||||||
|
let saved_sender = sender.clone();
|
||||||
|
thread::spawn(move || {
|
||||||
|
let mut state = event_loop::State::new(initial_state, Instant::now());
|
||||||
|
loop {
|
||||||
|
match receiver.recv() {
|
||||||
|
Ok(event) => {
|
||||||
|
state = Self::handle_loop_event(&sender, state, event, &ui);
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
logging::print(logging::Level::Bug, &format!("Senders hung up, aborting: {}", e));
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
thread: saved_sender,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send(&self, event: Event) -> Result<(), mpsc::SendError<Event>> {
|
||||||
|
self.thread.send(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_loop_event(loop_sender: &Sender, state: event_loop::State, event: Event, ui: &UISender)
|
||||||
|
-> event_loop::State
|
||||||
|
{
|
||||||
|
let now = Instant::now();
|
||||||
|
|
||||||
|
let (new_state, commands) = event_loop::handle_event(state.clone(), event, now);
|
||||||
|
|
||||||
|
ui.send(commands)
|
||||||
|
.or_warn(&mut logging::Print, logging::Problem::Bug, "Can't send to UI");
|
||||||
|
|
||||||
|
if new_state.scheduled_wakeup != state.scheduled_wakeup {
|
||||||
|
if let Some(when) = new_state.scheduled_wakeup {
|
||||||
|
Self::schedule_timeout_wake(loop_sender, when);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new_state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schedule_timeout_wake(loop_sender: &Sender, when: Instant) {
|
||||||
|
let sender = loop_sender.clone();
|
||||||
|
thread::spawn(move || {
|
||||||
|
let now = Instant::now();
|
||||||
|
thread::sleep(when - now);
|
||||||
|
sender.send(Event::TimeoutReached(when))
|
||||||
|
.or_warn(&mut logging::Print, logging::Problem::Warning, "Can't wake visibility manager");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For calling in only
|
||||||
|
mod c {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use crate::state::Presence;
|
||||||
|
use crate::state::visibility;
|
||||||
|
use crate::util::c::Wrapped;
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C"
|
||||||
|
fn squeek_state_send_force_visible(mgr: Wrapped<Threaded>) {
|
||||||
|
let sender = mgr.clone_ref();
|
||||||
|
let sender = sender.borrow();
|
||||||
|
sender.send(Event::Visibility(visibility::Event::ForceVisible))
|
||||||
|
.or_warn(&mut logging::Print, logging::Problem::Warning, "Can't send to state manager");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C"
|
||||||
|
fn squeek_state_send_force_hidden(sender: Wrapped<Threaded>) {
|
||||||
|
let sender = sender.clone_ref();
|
||||||
|
let sender = sender.borrow();
|
||||||
|
sender.send(Event::Visibility(visibility::Event::ForceHidden))
|
||||||
|
.or_warn(&mut logging::Print, logging::Problem::Warning, "Can't send to state manager");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C"
|
||||||
|
fn squeek_state_send_keyboard_present(sender: Wrapped<Threaded>, present: u32) {
|
||||||
|
let sender = sender.clone_ref();
|
||||||
|
let sender = sender.borrow();
|
||||||
|
let state =
|
||||||
|
if present == 0 { Presence::Missing }
|
||||||
|
else { Presence::Present };
|
||||||
|
sender.send(Event::PhysicalKeyboard(state))
|
||||||
|
.or_warn(&mut logging::Print, logging::Problem::Warning, "Can't send to state manager");
|
||||||
|
}
|
||||||
|
}
|
||||||
186
src/event_loop/mod.rs
Normal file
186
src/event_loop/mod.rs
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
/* Copyright (C) 2021 Purism SPC
|
||||||
|
* SPDX-License-Identifier: GPL-3.0+
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*! The loop abstraction for driving state changes.
|
||||||
|
* It binds to the state tracker in `state::Application`,
|
||||||
|
* and actually gets driven by a driver in the `driver` module.
|
||||||
|
*
|
||||||
|
* * * *
|
||||||
|
*
|
||||||
|
* If we performed updates in a tight loop,
|
||||||
|
* the state tracker would have been all we need.
|
||||||
|
*
|
||||||
|
* ``
|
||||||
|
* loop {
|
||||||
|
* event = current_event()
|
||||||
|
* outcome = update_state(event)
|
||||||
|
* io.apply(outcome)
|
||||||
|
* }
|
||||||
|
* ``
|
||||||
|
*
|
||||||
|
* This is enough to process all events,
|
||||||
|
* and keep the window always in sync with the current state.
|
||||||
|
*
|
||||||
|
* However, we're trying to be conservative,
|
||||||
|
* and not waste time performing updates that don't change state,
|
||||||
|
* so we have to react to events that end up influencing the state.
|
||||||
|
*
|
||||||
|
* One complication from that is that animation steps
|
||||||
|
* are not a response to events coming from the owner of the loop,
|
||||||
|
* but are needed by the loop itself.
|
||||||
|
*
|
||||||
|
* This is where the rest of bugs hide:
|
||||||
|
* too few scheduled wakeups mean missed updates and wrong visible state.
|
||||||
|
* Too many wakeups can slow down the process, or make animation jittery.
|
||||||
|
* The loop iteration is kept as a pure function to stay testable.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub mod driver;
|
||||||
|
|
||||||
|
// This module is tightly coupled to the shape of data passed around in this project.
|
||||||
|
// That's not a problem as long as there's only one loop.
|
||||||
|
// They can still be abstracted into Traits,
|
||||||
|
// and the loop parametrized over them.
|
||||||
|
use crate::main::Commands;
|
||||||
|
use crate::state;
|
||||||
|
use crate::state::Event;
|
||||||
|
use std::cmp;
|
||||||
|
use std::time::{ Duration, Instant };
|
||||||
|
|
||||||
|
|
||||||
|
/// This keeps the state of the tracker loop between iterations
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct State {
|
||||||
|
state: state::Application,
|
||||||
|
scheduled_wakeup: Option<Instant>,
|
||||||
|
last_update: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
fn new(initial_state: state::Application, now: Instant) -> Self {
|
||||||
|
Self {
|
||||||
|
state: initial_state,
|
||||||
|
scheduled_wakeup: None,
|
||||||
|
last_update: now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single iteration of the loop, updating its persistent state.
|
||||||
|
/// - updates tracker state,
|
||||||
|
/// - determines outcome,
|
||||||
|
/// - determines next scheduled animation wakeup,
|
||||||
|
/// and because this is a pure function, it's easily testable.
|
||||||
|
/// It returns the new state, and the message to send onwards.
|
||||||
|
fn handle_event(
|
||||||
|
mut loop_state: State,
|
||||||
|
event: Event,
|
||||||
|
now: Instant,
|
||||||
|
) -> (State, Commands) {
|
||||||
|
// Calculate changes to send to the consumer,
|
||||||
|
// based on publicly visible state.
|
||||||
|
// The internal state may change more often than the publicly visible one,
|
||||||
|
// so the resulting changes may be no-ops.
|
||||||
|
let old_state = loop_state.state.clone();
|
||||||
|
let last_update = loop_state.last_update;
|
||||||
|
loop_state.state = loop_state.state.apply_event(event.clone(), now);
|
||||||
|
loop_state.last_update = now;
|
||||||
|
|
||||||
|
let new_outcome = loop_state.state.get_outcome(now);
|
||||||
|
|
||||||
|
let commands = old_state.get_outcome(last_update)
|
||||||
|
.get_commands_to_reach(&new_outcome);
|
||||||
|
|
||||||
|
// Timeout events are special: they affect the scheduled timeout.
|
||||||
|
loop_state.scheduled_wakeup = match event {
|
||||||
|
Event::TimeoutReached(when) => {
|
||||||
|
if when > now {
|
||||||
|
// Special handling for scheduled events coming in early.
|
||||||
|
// Wait at least 10 ms to avoid Zeno's paradox.
|
||||||
|
// This is probably not needed though,
|
||||||
|
// if the `now` contains the desired time of the event.
|
||||||
|
// But then what about time "reversing"?
|
||||||
|
Some(cmp::max(
|
||||||
|
when,
|
||||||
|
now + Duration::from_millis(10),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
// There's only one timeout in flight, and it's this one.
|
||||||
|
// It's about to complete, and then the tracker can be cleared.
|
||||||
|
// I'm not sure if this is strictly necessary.
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => loop_state.scheduled_wakeup.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reschedule timeout if the new state calls for it.
|
||||||
|
let scheduled = &loop_state.scheduled_wakeup;
|
||||||
|
let desired = loop_state.state.get_next_wake(now);
|
||||||
|
|
||||||
|
loop_state.scheduled_wakeup = match (scheduled, desired) {
|
||||||
|
(&Some(scheduled), Some(next)) => {
|
||||||
|
if scheduled > next {
|
||||||
|
// State wants a wake to happen before the one which is already scheduled.
|
||||||
|
// The previous state is removed in order to only ever keep one in flight.
|
||||||
|
// That hopefully avoids pileups,
|
||||||
|
// e.g. because the system is busy
|
||||||
|
// and the user keeps doing something that queues more events.
|
||||||
|
Some(next)
|
||||||
|
} else {
|
||||||
|
// Not changing the case when the wanted wake is *after* scheduled,
|
||||||
|
// because wakes are not expensive as long as they don't pile up,
|
||||||
|
// and I can't see a pileup potential when it doesn't retrigger itself.
|
||||||
|
// Skipping an expected event is much more dangerous.
|
||||||
|
Some(scheduled)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(None, Some(next)) => Some(next),
|
||||||
|
// No need to change the unneeded wake - see above.
|
||||||
|
// (Some(_), None) => ...
|
||||||
|
(other, _) => other.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
(loop_state, commands)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
use crate::animation;
|
||||||
|
use crate::imservice::{ ContentHint, ContentPurpose };
|
||||||
|
use crate::main::PanelCommand;
|
||||||
|
use crate::state::{ Application, InputMethod, InputMethodDetails, Presence, visibility };
|
||||||
|
|
||||||
|
fn imdetails_new() -> InputMethodDetails {
|
||||||
|
InputMethodDetails {
|
||||||
|
purpose: ContentPurpose::Normal,
|
||||||
|
hint: ContentHint::NONE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn schedule_hide() {
|
||||||
|
let start = Instant::now(); // doesn't matter when. It would be better to have a reproducible value though
|
||||||
|
let mut now = start;
|
||||||
|
|
||||||
|
let state = Application {
|
||||||
|
im: InputMethod::Active(imdetails_new()),
|
||||||
|
physical_keyboard: Presence::Missing,
|
||||||
|
visibility_override: visibility::State::NotForced,
|
||||||
|
};
|
||||||
|
|
||||||
|
let l = State::new(state, now);
|
||||||
|
let (l, commands) = handle_event(l, InputMethod::InactiveSince(now).into(), now);
|
||||||
|
assert_eq!(commands.panel_visibility, Some(PanelCommand::Show));
|
||||||
|
assert_eq!(l.scheduled_wakeup, Some(now + animation::HIDING_TIMEOUT));
|
||||||
|
|
||||||
|
now += animation::HIDING_TIMEOUT;
|
||||||
|
|
||||||
|
let (l, commands) = handle_event(l, Event::TimeoutReached(now), now);
|
||||||
|
assert_eq!(commands.panel_visibility, Some(PanelCommand::Hide));
|
||||||
|
assert_eq!(l.scheduled_wakeup, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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) {
|
||||||
|
|||||||
@ -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,9 +27,6 @@ pub mod c {
|
|||||||
|
|
||||||
use std::os::raw::{c_char, c_void};
|
use std::os::raw::{c_char, c_void};
|
||||||
|
|
||||||
pub use ::ui_manager::c::UIManager;
|
|
||||||
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*
|
||||||
@ -40,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
|
||||||
@ -141,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 {
|
||||||
@ -150,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
|
||||||
@ -178,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
|
||||||
@ -329,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?
|
||||||
@ -347,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(),
|
||||||
@ -415,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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,17 +19,21 @@ extern crate xkbcommon;
|
|||||||
mod logging;
|
mod logging;
|
||||||
|
|
||||||
mod action;
|
mod action;
|
||||||
|
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;
|
||||||
mod layout;
|
mod layout;
|
||||||
mod locale;
|
mod locale;
|
||||||
|
mod main;
|
||||||
mod manager;
|
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;
|
||||||
|
|||||||
33
src/main.h
Normal file
33
src/main.h
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
/// 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 "dbus.h"
|
||||||
|
|
||||||
|
|
||||||
|
struct receiver;
|
||||||
|
|
||||||
|
/// Wrapped<event_loop::driver::Threaded>
|
||||||
|
struct squeek_state_manager;
|
||||||
|
|
||||||
|
struct submission;
|
||||||
|
|
||||||
|
struct rsobjects {
|
||||||
|
struct receiver *receiver;
|
||||||
|
struct squeek_state_manager *state_manager;
|
||||||
|
struct submission *submission;
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
149
src/main.rs
Normal file
149
src/main.rs
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
/* Copyright (C) 2020 Purism SPC
|
||||||
|
* SPDX-License-Identifier: GPL-3.0+
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*! Glue for the main loop. */
|
||||||
|
|
||||||
|
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 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)]
|
||||||
|
pub struct UIManager(*const c_void);
|
||||||
|
|
||||||
|
/// DbusHandler*
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct DBusHandler(*const c_void);
|
||||||
|
|
||||||
|
/// Holds the Rust structures that are interesting from C.
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct RsObjects {
|
||||||
|
receiver: Wrapped<Receiver<Commands>>,
|
||||||
|
state_manager: Wrapped<driver::Threaded>,
|
||||||
|
submission: Wrapped<Submission>,
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
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.
|
||||||
|
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 squeek_rsobjects_new(
|
||||||
|
im: *mut InputMethod,
|
||||||
|
vk: ZwpVirtualKeyboardV1,
|
||||||
|
) -> RsObjects {
|
||||||
|
let (sender, receiver) = MainContext::channel(PRIORITY_DEFAULT);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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<Commands>>,
|
||||||
|
ui_manager: *const UIManager,
|
||||||
|
dbus_handler: *const DBusHandler,
|
||||||
|
) {
|
||||||
|
let receiver = unsafe { receiver.unwrap() };
|
||||||
|
let receiver = Rc::try_unwrap(receiver).expect("References still present");
|
||||||
|
let receiver = receiver.into_inner();
|
||||||
|
let ctx = MainContext::default();
|
||||||
|
ctx.acquire();
|
||||||
|
receiver.attach(
|
||||||
|
Some(&ctx),
|
||||||
|
move |msg| {
|
||||||
|
main_loop_handle_message(msg, ui_manager, dbus_handler);
|
||||||
|
Continue(true)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ctx.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A single iteration of the UI loop.
|
||||||
|
/// Applies state outcomes to external portions of the program.
|
||||||
|
/// 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: Commands,
|
||||||
|
ui_manager: *const UIManager,
|
||||||
|
dbus_handler: *const DBusHandler,
|
||||||
|
) {
|
||||||
|
match msg.panel_visibility {
|
||||||
|
Some(PanelCommand::Show) => unsafe {
|
||||||
|
server_context_service_real_show_keyboard(ui_manager);
|
||||||
|
},
|
||||||
|
Some(PanelCommand::Hide) => unsafe {
|
||||||
|
server_context_service_real_hide_keyboard(ui_manager);
|
||||||
|
},
|
||||||
|
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>,
|
||||||
|
}
|
||||||
@ -30,7 +30,6 @@
|
|||||||
|
|
||||||
enum {
|
enum {
|
||||||
PROP_0,
|
PROP_0,
|
||||||
PROP_VISIBLE,
|
|
||||||
PROP_ENABLED,
|
PROP_ENABLED,
|
||||||
PROP_LAST
|
PROP_LAST
|
||||||
};
|
};
|
||||||
@ -43,12 +42,10 @@ 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
|
||||||
|
|
||||||
gboolean visible;
|
|
||||||
PhoshLayerSurface *window;
|
PhoshLayerSurface *window;
|
||||||
GtkWidget *widget; // nullable
|
GtkWidget *widget; // nullable
|
||||||
guint hiding;
|
|
||||||
guint last_requested_height;
|
guint last_requested_height;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -67,23 +64,6 @@ on_destroy (ServerContextService *self, GtkWidget *widget)
|
|||||||
//eekboard_context_service_destroy (EEKBOARD_CONTEXT_SERVICE (context));
|
//eekboard_context_service_destroy (EEKBOARD_CONTEXT_SERVICE (context));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
|
||||||
on_notify_map (ServerContextService *self, GtkWidget *widget)
|
|
||||||
{
|
|
||||||
g_return_if_fail (SERVER_IS_CONTEXT_SERVICE (self));
|
|
||||||
|
|
||||||
g_object_set (self, "visible", TRUE, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
static void
|
|
||||||
on_notify_unmap (ServerContextService *self, GtkWidget *widget)
|
|
||||||
{
|
|
||||||
g_return_if_fail (SERVER_IS_CONTEXT_SERVICE (self));
|
|
||||||
|
|
||||||
g_object_set (self, "visible", FALSE, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
static uint32_t
|
static uint32_t
|
||||||
calculate_height(int32_t width, GdkRectangle *geometry)
|
calculate_height(int32_t width, GdkRectangle *geometry)
|
||||||
{
|
{
|
||||||
@ -187,8 +167,6 @@ make_window (ServerContextService *self)
|
|||||||
|
|
||||||
g_object_connect (self->window,
|
g_object_connect (self->window,
|
||||||
"swapped-signal::destroy", G_CALLBACK(on_destroy), self,
|
"swapped-signal::destroy", G_CALLBACK(on_destroy), self,
|
||||||
"swapped-signal::map", G_CALLBACK(on_notify_map), self,
|
|
||||||
"swapped-signal::unmap", G_CALLBACK(on_notify_unmap), self,
|
|
||||||
"swapped-signal::configured", G_CALLBACK(on_surface_configure), self,
|
"swapped-signal::configured", G_CALLBACK(on_surface_configure), self,
|
||||||
NULL);
|
NULL);
|
||||||
|
|
||||||
@ -225,7 +203,8 @@ make_widget (ServerContextService *self)
|
|||||||
gtk_widget_show_all(self->widget);
|
gtk_widget_show_all(self->widget);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
// Called from rust
|
||||||
|
void
|
||||||
server_context_service_real_show_keyboard (ServerContextService *self)
|
server_context_service_real_show_keyboard (ServerContextService *self)
|
||||||
{
|
{
|
||||||
if (!self->window) {
|
if (!self->window) {
|
||||||
@ -234,99 +213,16 @@ server_context_service_real_show_keyboard (ServerContextService *self)
|
|||||||
if (!self->widget) {
|
if (!self->widget) {
|
||||||
make_widget (self);
|
make_widget (self);
|
||||||
}
|
}
|
||||||
self->visible = TRUE;
|
|
||||||
gtk_widget_show (GTK_WIDGET(self->window));
|
gtk_widget_show (GTK_WIDGET(self->window));
|
||||||
}
|
}
|
||||||
|
|
||||||
static gboolean
|
// Called from rust
|
||||||
show_keyboard_source_func(ServerContextService *context)
|
void
|
||||||
{
|
|
||||||
server_context_service_real_show_keyboard(context);
|
|
||||||
return G_SOURCE_REMOVE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static 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) {
|
||||||
self->visible = FALSE;
|
gtk_widget_hide (GTK_WIDGET(self->window));
|
||||||
}
|
|
||||||
|
|
||||||
static gboolean
|
|
||||||
hide_keyboard_source_func(ServerContextService *context)
|
|
||||||
{
|
|
||||||
server_context_service_real_hide_keyboard(context);
|
|
||||||
return G_SOURCE_REMOVE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static gboolean
|
|
||||||
on_hide (ServerContextService *self)
|
|
||||||
{
|
|
||||||
server_context_service_real_hide_keyboard(self);
|
|
||||||
self->hiding = 0;
|
|
||||||
|
|
||||||
return G_SOURCE_REMOVE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
server_context_service_show_keyboard (ServerContextService *self)
|
|
||||||
{
|
|
||||||
g_return_if_fail (SERVER_IS_CONTEXT_SERVICE(self));
|
|
||||||
|
|
||||||
if (self->hiding) {
|
|
||||||
g_source_remove (self->hiding);
|
|
||||||
self->hiding = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!self->visible) {
|
|
||||||
g_idle_add((GSourceFunc)show_keyboard_source_func, self);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
server_context_service_force_show_keyboard (ServerContextService *self)
|
|
||||||
{
|
|
||||||
if (!submission_hint_available(self->submission)) {
|
|
||||||
eekboard_context_service_set_hint_purpose(
|
|
||||||
self->state,
|
|
||||||
ZWP_TEXT_INPUT_V3_CONTENT_HINT_NONE,
|
|
||||||
ZWP_TEXT_INPUT_V3_CONTENT_PURPOSE_NORMAL
|
|
||||||
);
|
|
||||||
}
|
|
||||||
server_context_service_show_keyboard(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
server_context_service_hide_keyboard (ServerContextService *self)
|
|
||||||
{
|
|
||||||
g_return_if_fail (SERVER_IS_CONTEXT_SERVICE(self));
|
|
||||||
|
|
||||||
if (self->visible) {
|
|
||||||
g_idle_add((GSourceFunc)hide_keyboard_source_func, self);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Meant for use by the input-method handler:
|
|
||||||
/// the visible keyboard is no longer needed.
|
|
||||||
/// The implementation will delay it slightly,
|
|
||||||
/// because the release may be due to switching from one text field to another.
|
|
||||||
/// In this case, the user doesn't really need the keyboard surface
|
|
||||||
/// to disappear completely.
|
|
||||||
void
|
|
||||||
server_context_service_release_visibility (ServerContextService *self)
|
|
||||||
{
|
|
||||||
g_return_if_fail (SERVER_IS_CONTEXT_SERVICE(self));
|
|
||||||
|
|
||||||
if (!self->hiding && self->visible) {
|
|
||||||
self->hiding = g_timeout_add (200, (GSourceFunc) on_hide, self);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
@ -338,11 +234,8 @@ server_context_service_set_property (GObject *object,
|
|||||||
ServerContextService *self = SERVER_CONTEXT_SERVICE(object);
|
ServerContextService *self = SERVER_CONTEXT_SERVICE(object);
|
||||||
|
|
||||||
switch (prop_id) {
|
switch (prop_id) {
|
||||||
case PROP_VISIBLE:
|
|
||||||
self->visible = g_value_get_boolean (value);
|
|
||||||
break;
|
|
||||||
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);
|
||||||
@ -356,11 +249,7 @@ server_context_service_get_property (GObject *object,
|
|||||||
GValue *value,
|
GValue *value,
|
||||||
GParamSpec *pspec)
|
GParamSpec *pspec)
|
||||||
{
|
{
|
||||||
ServerContextService *self = SERVER_CONTEXT_SERVICE(object);
|
|
||||||
switch (prop_id) {
|
switch (prop_id) {
|
||||||
case PROP_VISIBLE:
|
|
||||||
g_value_set_boolean (value, self->visible);
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
||||||
break;
|
break;
|
||||||
@ -388,18 +277,6 @@ server_context_service_class_init (ServerContextServiceClass *klass)
|
|||||||
gobject_class->get_property = server_context_service_get_property;
|
gobject_class->get_property = server_context_service_get_property;
|
||||||
gobject_class->dispose = server_context_service_dispose;
|
gobject_class->dispose = server_context_service_dispose;
|
||||||
|
|
||||||
/**
|
|
||||||
* Flag to indicate if keyboard is visible or not.
|
|
||||||
*/
|
|
||||||
pspec = g_param_spec_boolean ("visible",
|
|
||||||
"Visible",
|
|
||||||
"Visible",
|
|
||||||
FALSE,
|
|
||||||
G_PARAM_READWRITE);
|
|
||||||
g_object_class_install_property (gobject_class,
|
|
||||||
PROP_VISIBLE,
|
|
||||||
pspec);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ServerContextServie:keyboard:
|
* ServerContextServie:keyboard:
|
||||||
*
|
*
|
||||||
@ -441,24 +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;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
// Used from Rust
|
||||||
server_context_service_update_visible (ServerContextService *self, gboolean visible) {
|
void server_context_service_set_hint_purpose(ServerContextService *self, uint32_t hint,
|
||||||
if (visible) {
|
uint32_t purpose) {
|
||||||
server_context_service_show_keyboard(self);
|
eekboard_context_service_set_hint_purpose(self->state, hint, purpose);
|
||||||
} else {
|
|
||||||
server_context_service_hide_keyboard(self);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -29,6 +29,7 @@
|
|||||||
#include "eekboard/eekboard-context-service.h"
|
#include "eekboard/eekboard-context-service.h"
|
||||||
#include "dbus.h"
|
#include "dbus.h"
|
||||||
#include "layout.h"
|
#include "layout.h"
|
||||||
|
#include "main.h"
|
||||||
#include "outputs.h"
|
#include "outputs.h"
|
||||||
#include "submission.h"
|
#include "submission.h"
|
||||||
#include "server-context-service.h"
|
#include "server-context-service.h"
|
||||||
@ -45,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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -280,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[] =
|
||||||
{
|
{
|
||||||
@ -375,6 +394,10 @@ 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,
|
||||||
|
instance.wayland.virtual_keyboard_manager,
|
||||||
|
instance.wayland.seat);
|
||||||
|
|
||||||
instance.ui_manager = squeek_uiman_new();
|
instance.ui_manager = squeek_uiman_new();
|
||||||
|
|
||||||
instance.settings_context = eekboard_context_service_new(&instance.layout_choice);
|
instance.settings_context = eekboard_context_service_new(&instance.layout_choice);
|
||||||
@ -394,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);
|
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");
|
||||||
@ -415,42 +438,30 @@ main (int argc, char **argv)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct vis_manager *vis_manager = squeek_visman_new();
|
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;
|
|
||||||
squeek_visman_set_ui(vis_manager, instance.ui_context);
|
|
||||||
|
|
||||||
if (instance.dbus_handler) {
|
instance.ui_context = ui_context;
|
||||||
dbus_handler_set_ui_context(instance.dbus_handler, instance.ui_context);
|
register_ui_loop_handler(rsobjects.receiver, instance.ui_context, instance.dbus_handler);
|
||||||
}
|
|
||||||
eekboard_context_service_set_ui(instance.settings_context, instance.ui_context);
|
|
||||||
|
|
||||||
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
417
src/state.rs
Normal 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),
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#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;
|
||||||
|
|
||||||
@ -14,7 +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(void);
|
struct vis_manager *squeek_visman_new(struct squeek_state_manager *state_manager);
|
||||||
void squeek_visman_set_ui(struct vis_manager *visman, ServerContextService *ui_context);
|
|
||||||
void squeek_visman_set_keyboard_present(struct vis_manager *visman, uint32_t keyboard_present);
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
/* Copyright (C) 2020 Purism SPC
|
/* Copyright (C) 2020, 2021 Purism SPC
|
||||||
* SPDX-License-Identifier: GPL-3.0+
|
* SPDX-License-Identifier: GPL-3.0+
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -10,49 +10,11 @@
|
|||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
use ::outputs::c::OutputHandle;
|
use ::outputs::c::OutputHandle;
|
||||||
|
|
||||||
|
|
||||||
pub mod c {
|
pub mod c {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::os::raw::c_void;
|
|
||||||
use ::util::c::Wrapped;
|
use ::util::c::Wrapped;
|
||||||
|
|
||||||
/// ServerContextService*
|
|
||||||
#[repr(transparent)]
|
|
||||||
pub struct UIManager(*const c_void);
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
pub fn server_context_service_update_visible(imservice: *const UIManager, active: u32);
|
|
||||||
pub fn server_context_service_release_visibility(imservice: *const UIManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C"
|
|
||||||
fn squeek_visman_new() -> Wrapped<VisibilityManager> {
|
|
||||||
Wrapped::new(VisibilityManager {
|
|
||||||
ui_manager: None,
|
|
||||||
visibility_state: VisibilityFactors {
|
|
||||||
im_active: false,
|
|
||||||
physical_keyboard_present: false,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Use to initialize the UI reference
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C"
|
|
||||||
fn squeek_visman_set_ui(visman: Wrapped<VisibilityManager>, ui_manager: *const UIManager) {
|
|
||||||
let visman = visman.clone_ref();
|
|
||||||
let mut visman = visman.borrow_mut();
|
|
||||||
visman.set_ui_manager(Some(ui_manager))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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> {
|
||||||
@ -118,131 +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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Temporary struct for migration. Should be integrated with Manager eventually.
|
|
||||||
pub struct VisibilityManager {
|
|
||||||
/// Owned reference. Be careful, it's shared with C at large
|
|
||||||
ui_manager: Option<*const c::UIManager>,
|
|
||||||
visibility_state: VisibilityFactors,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VisibilityManager {
|
|
||||||
fn set_ui_manager(&mut self, ui_manager: Option<*const c::UIManager>) {
|
|
||||||
let new = VisibilityManager {
|
|
||||||
ui_manager,
|
|
||||||
..unsafe { self.clone() }
|
|
||||||
};
|
|
||||||
self.apply_changes(new);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_changes(&mut self, new: Self) {
|
|
||||||
if let Some(ui) = &new.ui_manager {
|
|
||||||
if self.ui_manager.is_none() {
|
|
||||||
// Previous state was never applied, so effectively undefined.
|
|
||||||
// Just apply the new one.
|
|
||||||
let new_state = new.visibility_state.desired();
|
|
||||||
unsafe {
|
|
||||||
c::server_context_service_update_visible(
|
|
||||||
*ui,
|
|
||||||
(new_state == Visibility::Visible) as u32,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
match self.visibility_state.transition_to(&new.visibility_state) {
|
|
||||||
VisibilityTransition::Hide => unsafe {
|
|
||||||
c::server_context_service_update_visible(*ui, 0);
|
|
||||||
},
|
|
||||||
VisibilityTransition::Show => unsafe {
|
|
||||||
c::server_context_service_update_visible(*ui, 1);
|
|
||||||
},
|
|
||||||
VisibilityTransition::Release => unsafe {
|
|
||||||
c::server_context_service_release_visibility(*ui);
|
|
||||||
},
|
|
||||||
VisibilityTransition::NoTransition => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The struct is not really safe to clone due to the ui_manager reference.
|
|
||||||
/// This is only a helper for getting desired visibility.
|
|
||||||
unsafe fn clone(&self) -> Self {
|
|
||||||
VisibilityManager {
|
|
||||||
ui_manager: self.ui_manager.clone(),
|
|
||||||
visibility_state: self.visibility_state.clone(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user