Merge branch 'layouts' into 'master'

state: Select the layout

See merge request World/Phosh/squeekboard!553
This commit is contained in:
dcz
2022-06-14 15:23:14 +00:00
24 changed files with 475 additions and 345 deletions

23
src/actors/mod.rs Normal file
View File

@ -0,0 +1,23 @@
/* Copyright (C) 2022 Purism SPC
* SPDX-License-Identifier: GPL-3.0+
*/
/*! Actors are parts of Squeekboard containing state independent from the main application state.
Because main application state is meant to be immutable,
it cannot be referenced directly by pieces of logic
interacting with the environment.
Such impure logic is split away (actor's logic)
and combined with relevant pieces of state (actor state),
thus preserving the purity (and sometimes simplicity) of the main state.
Actors can communicate with the main state by sending it messages,
and by receiving updates from it.
*/
// TODO: move crate::panel into crate::actors::panel.
// Panel contains state and logic to protect the main state from getting flooded
// with low-level wayland and gtk sizing events.
pub mod popover;

40
src/actors/popover.rs Normal file
View File

@ -0,0 +1,40 @@
/* Copyright (C) 2022 Purism SPC
* SPDX-License-Identifier: GPL-3.0+
*/
/*! The popover is opened directly by the GTK surface,
without bouncing click events off the main state.
Then it must accurately show which layout has been selected.
It can get the system layout directly from gsettings on open,
but it cannot get the user-selected overlay, because it's stored in state.
To solve this, overlay will be cached in the popover actor,
and updated by main state every time it changes.
*/
pub mod c {
use super::*;
use crate::util::c::Wrapped;
/// The mutable instance of state
pub type Actor = Wrapped<State>;
}
#[derive(Clone)]
pub struct State {
pub overlay: Option<String>,
}
impl State {
pub fn new() -> Self {
Self { overlay: None }
}
}
pub fn set_overlay(
actor: &c::Actor,
overlay: Option<String>,
) {
let actor = actor.clone_ref();
let mut actor = actor.borrow_mut();
actor.overlay = overlay;
}

View File

@ -6,18 +6,30 @@
use std::time::Duration;
use crate::imservice::ContentPurpose;
use crate::layout::ArrangementKind;
use crate::outputs::OutputId;
use crate::panel::PixelSize;
/// The keyboard should hide after this has elapsed to prevent flickering.
pub const HIDING_TIMEOUT: Duration = Duration::from_millis(200);
/// Description of parameters which influence panel contents
#[derive(PartialEq, Clone, Debug)]
pub struct Contents {
pub name: String,
pub kind: ArrangementKind,
pub overlay_name: Option<String>,
pub purpose: ContentPurpose,
}
/// The outwardly visible state of visibility
#[derive(PartialEq, Debug, Clone)]
pub enum Outcome {
Visible {
output: OutputId,
height: PixelSize,
contents: Contents,
},
Hidden,
}

View File

@ -7,64 +7,16 @@
use std::env;
use std::fmt;
use std::path::PathBuf;
use std::convert::TryFrom;
use super::{ Error, LoadError };
use super::parsing;
use ::layout::ArrangementKind;
use ::logging;
use ::util::c::as_str;
use ::xdg;
use ::imservice::ContentPurpose;
use crate::layout;
use crate::layout::ArrangementKind;
use crate::logging;
use crate::xdg;
use crate::imservice::ContentPurpose;
// traits, derives
use ::logging::Warn;
/// Gathers stuff defined in C or called by C
pub mod c {
use super::*;
use std::os::raw::c_char;
#[no_mangle]
pub extern "C"
fn squeek_load_layout(
name: *const c_char, // name of the keyboard
type_: u32, // type like Wide
variant: u32, // purpose variant like numeric, terminal...
overlay: *const c_char, // the overlay (looking for "terminal")
) -> *mut ::layout::Layout {
let type_ = match type_ {
0 => ArrangementKind::Base,
1 => ArrangementKind::Wide,
_ => panic!("Bad enum value"),
};
let name = as_str(&name)
.expect("Bad layout name")
.expect("Empty layout name");
let variant = ContentPurpose::try_from(variant)
.or_print(
logging::Problem::Warning,
"Received invalid purpose value",
)
.unwrap_or(ContentPurpose::Normal);
let overlay_str = as_str(&overlay)
.expect("Bad overlay name")
.expect("Empty overlay name");
let overlay_str = match overlay_str {
"" => None,
other => Some(other),
};
let (kind, layout) = load_layout_data_with_fallback(&name, type_, variant, overlay_str);
let layout = ::layout::Layout::new(layout, kind, variant);
Box::into_raw(Box::new(layout))
}
}
const FALLBACK_LAYOUT_NAME: &str = "us";
@ -265,7 +217,7 @@ fn load_layout_data_with_fallback(
kind: ArrangementKind,
purpose: ContentPurpose,
overlay: Option<&str>,
) -> (ArrangementKind, ::layout::LayoutData) {
) -> (ArrangementKind, layout::LayoutData) {
// Build the path to the right keyboard layout subdirectory
let path = env::var_os("SQUEEKBOARD_KEYBOARDSDIR")
@ -300,6 +252,17 @@ fn load_layout_data_with_fallback(
panic!("No useful layout found!");
}
pub fn load_layout(
name: String,
kind: ArrangementKind,
variant: ContentPurpose,
overlay: Option<String>,
) -> layout::Layout {
let overlay = overlay.as_ref().map(String::as_str);
let (found_kind, layout)
= load_layout_data_with_fallback(&name, kind, variant, overlay);
layout::Layout::new(layout, found_kind, variant)
}
#[cfg(test)]
mod tests {

View File

@ -4,7 +4,7 @@
/*! Combined module for dealing with layout files */
mod loading;
pub mod loading;
pub mod parsing;
use std::io;

View File

@ -39,7 +39,9 @@ type UISender = glib::Sender<Commands>;
/// 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.
/// by creating a glib::Sender and checking what messages it receives.
// This can/should be abstracted over Event and Commands,
// so that the C call-ins can be thrown away from here and defined near events.
#[derive(Clone)]
pub struct Threaded {
thread: Sender,
@ -108,8 +110,11 @@ mod c {
use super::*;
use crate::state::Presence;
use crate::state::LayoutChoice;
use crate::state::visibility;
use crate::util;
use crate::util::c::Wrapped;
use std::os::raw::c_char;
#[no_mangle]
pub extern "C"
@ -140,4 +145,28 @@ mod c {
sender.send(Event::PhysicalKeyboard(state))
.or_warn(&mut logging::Print, logging::Problem::Warning, "Can't send to state manager");
}
#[no_mangle]
pub extern "C"
fn squeek_state_send_layout_set(
sender: Wrapped<Threaded>,
name: *const c_char,
source: *const c_char,
// TODO: use when synthetic events are needed
_timestamp: u32,
) {
let sender = sender.clone_ref();
let sender = sender.borrow();
let string_or_empty = |v| String::from(
util::c::as_str(v)
.unwrap_or(Some(""))
.unwrap_or("")
);
sender
.send(Event::LayoutChoice(LayoutChoice {
name: string_or_empty(&name),
source: string_or_empty(&source).into(),
}))
.or_warn(&mut logging::Print, logging::Problem::Warning, "Can't send to state manager");
}
}

View File

@ -226,7 +226,7 @@ bitflags!{
/// use rs::imservice::ContentPurpose;
/// assert_eq!(ContentPurpose::Alpha as u32, 1);
/// ```
#[derive(Debug, Copy, Clone)]
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum ContentPurpose {
Normal = 0,
Alpha = 1,

View File

@ -7,6 +7,8 @@
#include "eek/eek-gtk-keyboard.h"
#include "eek/eek-renderer.h"
#include "eek/eek-types.h"
#include "src/main.h"
#include "src/popover.h"
#include "src/submission.h"
#include "virtual-keyboard-unstable-v1-client-protocol.h"
#include "text-input-unstable-v3-client-protocol.h"
@ -40,7 +42,8 @@ void squeek_layout_release(struct squeek_layout *layout,
struct submission *submission,
struct transformation widget_to_layout,
uint32_t timestamp,
EekboardContextService *manager,
struct squeek_popover *popover,
struct squeek_state_manager *state,
EekGtkKeyboard *ui_keyboard);
void squeek_layout_release_all_only(struct squeek_layout *layout,
struct submission *submission,
@ -54,7 +57,8 @@ void squeek_layout_drag(struct squeek_layout *layout,
struct submission *submission,
double x_widget, double y_widget,
struct transformation widget_to_layout,
uint32_t timestamp, EekboardContextService *manager,
uint32_t timestamp, struct squeek_popover *popover,
struct squeek_state_manager *state,
EekGtkKeyboard *ui_keyboard);
void squeek_layout_draw_all_changed(struct squeek_layout *layout, EekRenderer* renderer, cairo_t *cr, struct submission *submission);
void squeek_draw_layout_base_view(struct squeek_layout *layout, EekRenderer* renderer, cairo_t *cr);

View File

@ -25,31 +25,36 @@ use std::fmt;
use std::rc::Rc;
use std::vec::Vec;
use ::action::Action;
use ::drawing;
use ::float_ord::FloatOrd;
use ::keyboard::KeyState;
use ::logging;
use ::manager;
use ::submission::{ Submission, SubmitData, Timestamp };
use ::util::find_max_double;
use crate::action::Action;
use crate::actors;
use crate::drawing;
use crate::float_ord::FloatOrd;
use crate::keyboard::KeyState;
use crate::logging;
use crate::popover;
use crate::receiver;
use crate::submission::{ Submission, SubmitData, Timestamp };
use crate::util::find_max_double;
use ::imservice::ContentPurpose;
use crate::imservice::ContentPurpose;
// Traits
use std::borrow::Borrow;
use ::logging::Warn;
use crate::logging::Warn;
/// Gathers stuff defined in C or called by C
pub mod c {
use super::*;
use gtk_sys;
use std::os::raw::c_void;
use crate::receiver;
use crate::submission::c::Submission as CSubmission;
use gtk_sys;
use std::ops::{ Add, Sub };
use std::os::raw::c_void;
use crate::util::CloneOwned;
// The following defined in C
#[repr(transparent)]
#[derive(Copy, Clone)]
@ -215,13 +220,17 @@ pub mod c {
submission: CSubmission,
widget_to_layout: Transformation,
time: u32,
manager: manager::c::Manager,
popover: actors::popover::c::Actor,
app_state: receiver::c::State,
ui_keyboard: EekGtkKeyboard,
) {
let time = Timestamp(time);
let layout = unsafe { &mut *layout };
let submission = submission.clone_ref();
let mut submission = submission.borrow_mut();
let app_state = app_state.clone_owned();
let popover_state = popover.clone_owned();
let ui_backend = UIBackend {
widget_to_layout,
keyboard: ui_keyboard,
@ -236,7 +245,7 @@ pub mod c {
&mut submission,
Some(&ui_backend),
time,
Some(manager),
Some((&popover_state, app_state.clone())),
key,
);
}
@ -315,13 +324,18 @@ pub mod c {
x_widget: f64, y_widget: f64,
widget_to_layout: Transformation,
time: u32,
manager: manager::c::Manager,
popover: actors::popover::c::Actor,
app_state: receiver::c::State,
ui_keyboard: EekGtkKeyboard,
) {
let time = Timestamp(time);
let layout = unsafe { &mut *layout };
let submission = submission.clone_ref();
let mut submission = submission.borrow_mut();
// We only need to query state here, not update.
// A copy is enough.
let popover_state = popover.clone_owned();
let app_state = app_state.clone_owned();
let ui_backend = UIBackend {
widget_to_layout,
keyboard: ui_keyboard,
@ -352,7 +366,7 @@ pub mod c {
&mut submission,
Some(&ui_backend),
time,
Some(manager),
Some((&popover_state, app_state.clone())),
key,
);
}
@ -377,7 +391,7 @@ pub mod c {
&mut submission,
Some(&ui_backend),
time,
Some(manager),
Some((&popover_state, app_state.clone())),
key,
);
}
@ -1035,7 +1049,11 @@ mod seat {
submission: &mut Submission,
ui: Option<&UIBackend>,
time: Timestamp,
manager: Option<manager::c::Manager>,
// TODO: intermediate measure:
// passing state conditionally because it's only used for popover.
// Eventually, it should be used for sumitting button events,
// and passed always.
manager: Option<(&actors::popover::State, receiver::State)>,
rckey: &Rc<RefCell<KeyState>>,
) {
let key: KeyState = {
@ -1070,7 +1088,7 @@ mod seat {
// only show when UI is present
Action::ShowPreferences => if let Some(ui) = &ui {
// only show when layout manager is available
if let Some(manager) = manager {
if let Some((manager, app_state)) = manager {
let view = layout.get_current_view();
let places = ::layout::procedures::find_key_places(
view, &rckey,
@ -1085,10 +1103,11 @@ mod seat {
width: button.size.width,
height: button.size.height,
};
::popover::show(
popover::show(
ui.keyboard,
ui.widget_to_layout.reverse_bounds(bounds),
manager,
app_state,
);
}
}

View File

@ -23,6 +23,7 @@ mod assert_matches;
mod logging;
mod action;
mod actors;
mod animation;
pub mod data;
mod debug;
@ -34,10 +35,10 @@ mod keyboard;
mod layout;
mod locale;
mod main;
mod manager;
mod outputs;
mod panel;
mod popover;
mod receiver;
mod resources;
mod state;
mod style;

View File

@ -9,6 +9,7 @@
#include "eek/eek-types.h"
#include "dbus.h"
#include "panel.h"
#include "src/popover.h"
struct receiver;
@ -23,9 +24,10 @@ struct rsobjects {
struct squeek_state_manager *state_manager;
struct submission *submission;
struct squeek_wayland *wayland;
struct squeek_popover *popover;
};
void register_ui_loop_handler(struct receiver *receiver, struct panel_manager *panel, EekboardContextService *hint_manager, DBusHandler *dbus_handler);
void register_ui_loop_handler(struct receiver *receiver, struct panel_manager *panel, struct squeek_popover *popover, EekboardContextService *hint_manager, DBusHandler *dbus_handler);
struct rsobjects squeek_init(void);
@ -33,3 +35,4 @@ 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);
void squeek_state_send_layout_set(struct squeek_state_manager *state, char *name, char *layout, uint32_t timestamp);

View File

@ -3,9 +3,11 @@
*/
/*! Glue for the main loop. */
use crate::panel;
use crate::actors;
use crate::animation;
use crate::debug;
use crate::state;
use crate::data::loading;
use crate::panel;
use glib::{Continue, MainContext, PRIORITY_DEFAULT, Receiver};
@ -19,6 +21,7 @@ mod c {
use crate::event_loop::driver;
use crate::imservice::IMService;
use crate::imservice::c::InputMethod;
use crate::layout;
use crate::outputs::Outputs;
use crate::state;
use crate::submission::Submission;
@ -46,6 +49,7 @@ mod c {
submission: Wrapped<Submission>,
/// Not wrapped, because C needs to access this.
wayland: *mut Wayland,
popover: actors::popover::c::Actor,
}
/// Corresponds to wayland.h::squeek_wayland.
@ -78,7 +82,8 @@ mod c {
extern "C" {
#[allow(improper_ctypes)]
fn init_wayland(wayland: *mut Wayland);
fn eekboard_context_service_set_hint_purpose(service: HintManager, hint: u32, purpose: u32);
#[allow(improper_ctypes)]
fn eekboard_context_service_set_layout(service: HintManager, layout: *const layout::Layout, timestamp: 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);
@ -116,6 +121,7 @@ mod c {
state_manager: Wrapped::new(state_manager),
receiver: Wrapped::new(receiver),
wayland: Box::into_raw(wayland),
popover: Wrapped::new(actors::popover::State::new()),
}
}
@ -125,6 +131,7 @@ mod c {
fn register_ui_loop_handler(
receiver: Wrapped<Receiver<Commands>>,
panel_manager: panel::c::PanelManager,
popover: actors::popover::c::Actor,
hint_manager: HintManager,
dbus_handler: *const DBusHandler,
) {
@ -137,7 +144,13 @@ mod c {
receiver.attach(
Some(&ctx),
move |msg| {
main_loop_handle_message(msg, panel_manager.clone(), hint_manager, dbus_handler);
main_loop_handle_message(
msg,
panel_manager.clone(),
&popover,
hint_manager,
dbus_handler,
);
Continue(true)
},
);
@ -152,6 +165,7 @@ mod c {
fn main_loop_handle_message(
msg: Commands,
panel_manager: Wrapped<panel::Manager>,
popover: &actors::popover::c::Actor,
hint_manager: HintManager,
dbus_handler: *const DBusHandler,
) {
@ -164,24 +178,37 @@ mod c {
unsafe { dbus_handler_set_visible(dbus_handler, visible as u8) };
}
}
if let Some(hints) = msg.layout_hint_set {
if let Some(commands::SetLayout { description }) = msg.layout_selection {
let animation::Contents {
name,
kind,
overlay_name,
purpose,
} = description;
actors::popover::set_overlay(popover, overlay_name.clone());
let layout = loading::load_layout(name, kind, purpose, overlay_name);
let layout = Box::into_raw(Box::new(layout));
unsafe {
eekboard_context_service_set_hint_purpose(
hint_manager,
hints.hint.bits(),
hints.purpose.clone() as u32,
)
};
eekboard_context_service_set_layout(hint_manager, layout, 0);
}
}
}
}
pub mod commands {
use crate::animation;
#[derive(Clone, Debug)]
pub struct SetLayout {
pub description: animation::Contents,
}
}
/// The commands consumed by the main loop,
/// to be sent out to external components.
#[derive(Clone)]
pub struct Commands {
pub panel_visibility: Option<panel::Command>,
pub layout_hint_set: Option<state::InputMethodDetails>,
pub dbus_visible_set: Option<bool>,
pub layout_selection: Option<commands::SetLayout>,
}

View File

@ -1,33 +0,0 @@
/*! Procedures relating to the management of the switching of layouts */
use ::util;
pub mod c {
use std::os::raw::{c_char, c_void};
/// EekboardContextService*
#[repr(transparent)]
#[derive(Clone, Copy)]
pub struct Manager(*const c_void);
extern "C" {
pub fn eekboard_context_service_set_overlay(
manager: Manager,
name: *const c_char,
);
pub fn eekboard_context_service_get_overlay(
manager: Manager,
) -> *const c_char;
}
}
/// Returns the overlay name.
/// The result lifetime is "as long as the C copy lives"
pub fn get_overlay(manager: c::Manager) -> Option<String> {
let raw_str = unsafe {
c::eekboard_context_service_get_overlay(manager)
};
// this string is generated from Rust, should never be invalid
util::c::as_str(&raw_str).unwrap()
.map(String::from)
}

View File

@ -52,7 +52,7 @@ make_widget (struct panel_manager *self)
if (self->widget) {
g_error("Widget already present");
}
self->widget = eek_gtk_keyboard_new (self->state, self->submission, self->layout);
self->widget = eek_gtk_keyboard_new (self->state, self->submission, self->state_manager, self->popover);
gtk_widget_set_has_tooltip (self->widget, TRUE);
gtk_container_add (GTK_CONTAINER(self->window), self->widget);
@ -116,15 +116,16 @@ panel_manager_resize (struct panel_manager *self, uint32_t height)
}
struct panel_manager panel_manager_new(EekboardContextService *state, struct submission *submission, struct squeek_layout_state *layout)
struct panel_manager panel_manager_new(EekboardContextService *state, struct submission *submission, struct squeek_state_manager *state_manager, struct squeek_popover *popover)
{
struct panel_manager mgr = {
.state = state,
.submission = submission,
.layout = layout,
.window = NULL,
.widget = NULL,
.current_output = NULL,
.state_manager = state_manager,
.popover = popover,
};
return mgr;
}

View File

@ -2,14 +2,16 @@
#include "eek/layersurface.h"
#include "src/layout.h"
#include "src/main.h"
#include "src/submission.h"
// Stores the objects that the panel and its widget will refer to
struct panel_manager {
EekboardContextService *state; // unowned
/// Needed for instantiating the widget
struct squeek_state_manager *state_manager; // shared reference
struct squeek_popover *popover; // shared reference
struct submission *submission; // unowned
struct squeek_layout_state *layout;
PhoshLayerSurface *window;
GtkWidget *widget; // nullable
@ -18,4 +20,4 @@ struct panel_manager {
struct wl_output *current_output;
};
struct panel_manager panel_manager_new(EekboardContextService *state, struct submission *submission, struct squeek_layout_state *layout);
struct panel_manager panel_manager_new(EekboardContextService *state, struct submission *submission, struct squeek_state_manager *state_manager, struct squeek_popover *popover);

5
src/popover.h Normal file
View File

@ -0,0 +1,5 @@
#pragma once
/// Popover state.
/// Wrapped<actors::popover::State>
struct squeek_popover;

View File

@ -4,11 +4,13 @@ use gio;
use gtk;
use std::ffi::CString;
use std::cmp::Ordering;
use ::layout::c::{ Bounds, EekGtkKeyboard };
use ::locale::{ OwnedTranslation, compare_current_locale };
use ::logging;
use ::manager;
use ::resources;
use crate::actors;
use crate::layout::c::{ Bounds, EekGtkKeyboard };
use crate::locale::{ OwnedTranslation, compare_current_locale };
use crate::logging;
use crate::receiver;
use crate::resources;
use crate::state;
// Traits
use gio::prelude::ActionMapExt;
@ -16,7 +18,7 @@ use gio::prelude::SettingsExt;
use glib::translate::FromGlibPtrNone;
use glib::variant::ToVariant;
use gtk::prelude::*;
use ::logging::Warn;
use crate::logging::Warn;
mod c {
use std::os::raw::c_char;
@ -127,9 +129,11 @@ fn get_settings(schema_name: &str) -> Option<gio::Settings> {
.map(|_sschema| gio::Settings::new(schema_name))
}
fn set_layout(kind: String, name: String) {
fn set_layout(kind: &str, name: &str) {
let settings = get_settings("org.gnome.desktop.input-sources");
if let Some(settings) = settings {
let kind = String::from(kind);
let name = String::from(name);
#[cfg(feature = "glib_v0_14")]
let inputs = settings.value("sources");
#[cfg(not(feature = "glib_v0_14"))]
@ -150,7 +154,7 @@ fn set_layout(kind: String, name: String) {
/// A reference to what the user wants to see
#[derive(PartialEq, Clone, Debug)]
enum LayoutId {
pub enum LayoutId {
/// Affects the layout in system settings
System {
kind: String,
@ -170,40 +174,23 @@ impl LayoutId {
}
fn set_visible_layout(
manager: manager::c::Manager,
layout_id: LayoutId,
layout_id: &LayoutId,
) {
match layout_id {
LayoutId::System { kind, name } => {
unsafe {
use std::ptr;
manager::c::eekboard_context_service_set_overlay(
manager,
ptr::null(),
);
}
set_layout(kind, name);
}
LayoutId::Local(name) => {
let name = CString::new(name.as_str()).unwrap();
let name_ptr = name.as_ptr();
unsafe {
manager::c::eekboard_context_service_set_overlay(
manager,
name_ptr,
)
}
},
_ => {},
}
}
/// Takes into account first any overlays, then system layouts from the list
fn get_current_layout(
manager: manager::c::Manager,
popover: &actors::popover::State,
system_layouts: &Vec<LayoutId>,
) -> Option<LayoutId> {
match manager::get_overlay(manager) {
Some(name) => Some(LayoutId::Local(name)),
match &popover.overlay {
Some(name) => Some(LayoutId::Local(name.into())),
None => system_layouts.get(0).map(LayoutId::clone),
}
}
@ -247,7 +234,8 @@ fn translate_layout_names(layouts: &Vec<LayoutId>) -> Vec<OwnedTranslation> {
pub fn show(
window: EekGtkKeyboard,
position: Bounds,
manager: manager::c::Manager,
popover: &actors::popover::State,
app_state: receiver::State,
) {
unsafe { gtk::set_initialized() };
let window = unsafe { gtk::Widget::from_glib_none(window.0) };
@ -327,7 +315,7 @@ pub fn show(
let action_group = gio::SimpleActionGroup::new();
if let Some(current_layout) = get_current_layout(manager, &system_layouts) {
if let Some(current_layout) = get_current_layout(popover, &system_layouts) {
let current_layout_name = all_layouts.iter()
.find(
|l| l.get_name() == current_layout.get_name()
@ -356,10 +344,13 @@ pub fn show(
.find(
|choices| state == choices.get_name()
).unwrap();
set_visible_layout(
manager,
layout.clone(),
)
app_state
.send(state::Event::OverlayChanged(layout.clone()))
.or_print(
logging::Problem::Bug,
&format!("Can't send to state"),
);
set_visible_layout(layout)
});
},
None => log_print!(

15
src/receiver.rs Normal file
View File

@ -0,0 +1,15 @@
/*! Defines the application-wide message bus for updating state.*/
use crate::event_loop::driver::Threaded;
pub mod c {
use super::*;
use crate::util::c::Wrapped;
pub type State = Wrapped<Threaded>;
}
// The state receiver is an endpoint of a channel, so it's safely cloneable.
// There's no need to keep it in a Rc.
// The C version uses Wrapped with an underlying Rc,
// because Wrapped is well-tested already.
pub type State = Threaded;

View File

@ -56,8 +56,6 @@ struct squeekboard {
/// Gsettings hook for visibility. TODO: this does not belong in gsettings.
ServerContextService *settings_handler;
struct panel_manager panel_manager; // Controls the shape of the panel.
/// Currently wanted layout. TODO: merge into state::Application
struct squeek_layout_state layout_choice;
};
@ -400,7 +398,7 @@ main (int argc, char **argv)
// Also initializes wayland
struct rsobjects rsobjects = squeek_init();
instance.settings_context = eekboard_context_service_new(&instance.layout_choice);
instance.settings_context = eekboard_context_service_new(rsobjects.state_manager);
// set up dbus
@ -450,9 +448,10 @@ main (int argc, char **argv)
instance.panel_manager = panel_manager_new(instance.settings_context,
rsobjects.submission,
&instance.layout_choice);
rsobjects.state_manager,
rsobjects.popover);
register_ui_loop_handler(rsobjects.receiver, &instance.panel_manager, instance.settings_context, instance.dbus_handler);
register_ui_loop_handler(rsobjects.receiver, &instance.panel_manager, rsobjects.popover, instance.settings_context, instance.dbus_handler);
session_register();

View File

@ -1,4 +1,4 @@
/* Copyright (C) 2021 Purism SPC
/* Copyright (C) 2021,2022 Purism SPC
* SPDX-License-Identifier: GPL-3.0+
*/
@ -8,11 +8,14 @@
use crate::animation;
use crate::debug;
use crate::imservice::{ ContentHint, ContentPurpose };
use crate::layout::ArrangementKind;
use crate::main;
use crate::main::Commands;
use crate::outputs;
use crate::outputs::{Millimeter, OutputId, OutputState};
use crate::panel;
use crate::panel::PixelSize;
use crate::popover;
use crate::util::Rational;
use std::cmp;
use std::collections::HashMap;
@ -37,6 +40,29 @@ pub enum InputMethod {
InactiveSince(Instant),
}
#[derive(Clone, Debug)]
pub enum LayoutSource {
Xkb,
Other(String),
}
impl From<String> for LayoutSource {
fn from(v: String) -> Self {
if v.as_str() == "xkb" {
LayoutSource::Xkb
} else {
LayoutSource::Other(v)
}
}
}
/// The user's preferred system layout
#[derive(Clone, Debug)]
pub struct LayoutChoice {
pub name: String,
pub source: LayoutSource,
}
/// Incoming events.
/// This contains events that cause a change to the internal state.
#[derive(Clone, Debug)]
@ -45,6 +71,8 @@ pub enum Event {
Visibility(visibility::Event),
PhysicalKeyboard(Presence),
Output(outputs::Event),
LayoutChoice(LayoutChoice),
OverlayChanged(popover::LayoutId),
Debug(debug::Event),
/// Event triggered because a moment in time passed.
/// Use to animate state transitions.
@ -87,7 +115,7 @@ pub mod visibility {
/// The outwardly visible state.
#[derive(Clone, Debug)]
pub struct Outcome {
pub visibility: animation::Outcome,
pub panel: animation::Outcome,
pub im: InputMethod,
}
@ -98,36 +126,40 @@ impl Outcome {
/// 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,
};
// FIXME: handle switching outputs
let (dbus_visible_set, panel_visibility) = match new_state.visibility {
animation::Outcome::Visible{output, height}
let (dbus_visible_set, panel_visibility) = match new_state.panel {
animation::Outcome::Visible{output, height, ..}
=> (Some(true), Some(panel::Command::Show{output, height})),
animation::Outcome::Hidden => (Some(false), Some(panel::Command::Hide)),
};
// Compare the old and new states as not to flood with updates,
// which may look up in the file system.
use animation::Outcome::*;
let layout_selection = match &new_state.panel {
Visible{ contents: new_contents, ..} => {
let same
= if let Visible { contents, .. } = &self.panel {
contents == new_contents
} else {
false
};
if !same {
Some(main::commands::SetLayout {
description: new_contents.clone()
})
} else {
None
}
},
animation::Outcome::Hidden => None,
};
Commands {
panel_visibility,
layout_hint_set,
dbus_visible_set,
layout_selection,
}
}
}
@ -156,6 +188,13 @@ pub struct Application {
/// but not sure about being allowed on non-touch displays.
pub preferred_output: Option<OutputId>,
pub outputs: HashMap<OutputId, OutputState>,
/// We presume that the system always has some preference,
/// even though we receive the preference after init,
/// and we might not receive one at all (gsettings missing).
/// Then a default is used.
pub layout_choice: LayoutChoice,
/// Manual override of the system layout
pub overlay_layout: Option<popover::LayoutId>,
}
impl Application {
@ -173,6 +212,11 @@ impl Application {
debug_mode_enabled: false,
preferred_output: None,
outputs: Default::default(),
layout_choice: LayoutChoice {
name: String::from("us"),
source: LayoutSource::Xkb,
},
overlay_layout: None,
}
}
@ -257,7 +301,18 @@ impl Application {
im: InputMethod::InactiveSince(old),
..self
},
}
},
Event::LayoutChoice(layout_choice) => Self {
layout_choice,
overlay_layout: None,
..self
},
Event::OverlayChanged(overlay_layout) => Self {
overlay_layout: Some(overlay_layout),
..self
},
};
if state.debug_mode_enabled {
@ -273,7 +328,9 @@ Outcome:
state
}
fn get_preferred_height(output: &OutputState) -> Option<PixelSize> {
fn get_preferred_height_and_arrangement(output: &OutputState)
-> Option<(PixelSize, ArrangementKind)>
{
output.get_pixel_size()
.map(|px_size| {
// Assume isotropy.
@ -301,61 +358,95 @@ Outcome:
// TODO: calculate based on selected layout
const ROW_COUNT: u32 = 4;
let height = {
let ideal_height = IDEAL_TARGET_SIZE * ROW_COUNT as i32;
let ideal_height_px = (ideal_height * density).ceil().0 as u32;
let ideal_height = IDEAL_TARGET_SIZE * ROW_COUNT as i32;
let ideal_height_px = (ideal_height * density).ceil().0 as u32;
// Reduce height to match what the layout can fill.
// For this, we need to guess if normal or wide will be picked up.
// This must match `eek_gtk_keyboard.c::get_type`.
// TODO: query layout database and choose one directly
let abstract_width
= PixelSize {
scale_factor: output.scale as u32,
pixels: px_size.width,
}
.as_scaled_ceiling();
// Reduce height to match what the layout can fill.
// For this, we need to guess if normal or wide will be picked up.
// This must match `eek_gtk_keyboard.c::get_type`.
// TODO: query layout database and choose one directly
let abstract_width
= PixelSize {
scale_factor: output.scale as u32,
pixels: px_size.width,
}
.as_scaled_ceiling();
let height_as_widths = {
if abstract_width < 540 {
// Normal
Rational {
numerator: 210,
denominator: 360,
}
} else {
// Wide
Rational {
numerator: 172,
denominator: 540,
}
let (arrangement, height_as_widths) = {
if abstract_width < 540 {(
ArrangementKind::Base,
Rational {
numerator: 210,
denominator: 360,
},
)} else {(
ArrangementKind::Wide,
Rational {
numerator: 172,
denominator: 540,
}
};
cmp::min(
)}
};
let height
= cmp::min(
ideal_height_px,
(height_as_widths * px_size.width as i32).ceil() as u32,
)
};
PixelSize {
scale_factor: output.scale as u32,
pixels: cmp::min(height, px_size.height / 2),
}
);
(
PixelSize {
scale_factor: output.scale as u32,
pixels: cmp::min(height, px_size.height / 2),
},
arrangement,
)
})
}
/// Returns layout name, overlay name
fn get_layout_names(&self) -> (String, Option<String>) {
(
String::from(match &self.overlay_layout {
Some(popover::LayoutId::System { name, .. }) => name,
_ => &self.layout_choice.name,
}),
match &self.overlay_layout {
Some(popover::LayoutId::Local(name)) => Some(name.clone()),
_ => None,
},
)
}
pub fn get_outcome(&self, now: Instant) -> Outcome {
// FIXME: include physical keyboard presence
Outcome {
visibility: match self.preferred_output {
panel: match self.preferred_output {
None => animation::Outcome::Hidden,
Some(output) => {
// Hoping that this will get optimized out on branches not using `visible`.
let height = Self::get_preferred_height(self.outputs.get(&output).unwrap())
.unwrap_or(PixelSize{pixels: 0, scale_factor: 1});
let (height, arrangement) = Self::get_preferred_height_and_arrangement(self.outputs.get(&output).unwrap())
.unwrap_or((
PixelSize{pixels: 0, scale_factor: 1},
ArrangementKind::Base,
));
let (layout_name, overlay) = self.get_layout_names();
// TODO: Instead of setting size to 0 when the output is invalid,
// simply go invisible.
let visible = animation::Outcome::Visible{ output, height };
let visible = animation::Outcome::Visible{
output,
height,
contents: animation::Contents {
kind: arrangement,
name: layout_name,
overlay_name: overlay,
purpose: match self.im {
InputMethod::Active(InputMethodDetails { purpose, .. }) => purpose,
InputMethod::InactiveSince(_) => ContentPurpose::Normal,
},
}
};
match (self.physical_keyboard, self.visibility_override) {
(_, visibility::State::ForcedHidden) => animation::Outcome::Hidden,
(_, visibility::State::ForcedVisible) => visible,
@ -446,7 +537,7 @@ pub mod test {
for _i in 0..100 {
now += Duration::from_millis(1);
assert_matches!(
state.get_outcome(now).visibility,
state.get_outcome(now).panel,
animation::Outcome::Visible{..},
"Hidden when it should remain visible: {:?}",
now.saturating_duration_since(start),
@ -455,7 +546,10 @@ pub mod test {
let state = state.apply_event(Event::InputMethod(InputMethod::Active(imdetails_new())), now);
assert_matches!(state.get_outcome(now).visibility, animation::Outcome::Visible{..});
assert_matches!(
state.get_outcome(now).panel,
animation::Outcome::Visible{..}
);
}
/// Make sure that hiding works when input method goes away
@ -472,7 +566,7 @@ pub mod test {
let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);
while let animation::Outcome::Visible{..} = state.get_outcome(now).visibility {
while let animation::Outcome::Visible{..} = state.get_outcome(now).panel {
now += Duration::from_millis(1);
assert!(
now < start + Duration::from_millis(250),
@ -502,7 +596,7 @@ pub mod test {
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 {
while let animation::Outcome::Visible{..} = state.get_outcome(now).panel {
now += Duration::from_millis(1);
assert!(
now < start + Duration::from_millis(250),
@ -515,7 +609,7 @@ pub mod test {
for _i in 0..1000 {
now += Duration::from_millis(1);
assert_eq!(
state.get_outcome(now).visibility,
state.get_outcome(now).panel,
animation::Outcome::Hidden,
"Appeared unnecessarily: {:?}",
now.saturating_duration_since(start),
@ -537,7 +631,7 @@ pub mod test {
let state = state.apply_event(Event::Visibility(visibility::Event::ForceVisible), now);
assert_matches!(
state.get_outcome(now).visibility,
state.get_outcome(now).panel,
animation::Outcome::Visible{..},
"Failed to show: {:?}",
now.saturating_duration_since(start),
@ -550,7 +644,7 @@ pub mod test {
now += Duration::from_secs(1);
assert_eq!(
state.get_outcome(now).visibility,
state.get_outcome(now).panel,
animation::Outcome::Hidden,
"Failed to release forced visibility: {:?}",
now.saturating_duration_since(start),
@ -571,7 +665,7 @@ pub mod test {
let state = state.apply_event(Event::PhysicalKeyboard(Presence::Present), now);
assert_eq!(
state.get_outcome(now).visibility,
state.get_outcome(now).panel,
animation::Outcome::Hidden,
"Failed to hide: {:?}",
now.saturating_duration_since(start),
@ -583,7 +677,7 @@ pub mod test {
let state = state.apply_event(Event::InputMethod(InputMethod::Active(imdetails_new())), now);
assert_eq!(
state.get_outcome(now).visibility,
state.get_outcome(now).panel,
animation::Outcome::Hidden,
"Failed to remain hidden: {:?}",
now.saturating_duration_since(start),
@ -593,7 +687,7 @@ pub mod test {
let state = state.apply_event(Event::PhysicalKeyboard(Presence::Missing), now);
assert_matches!(
state.get_outcome(now).visibility,
state.get_outcome(now).panel,
animation::Outcome::Visible{..},
"Failed to appear: {:?}",
now.saturating_duration_since(start),
@ -605,7 +699,7 @@ pub mod test {
fn size_l5() {
use crate::outputs::{Mode, Geometry, c, Size};
assert_eq!(
Application::get_preferred_height(&OutputState {
Application::get_preferred_height_and_arrangement(&OutputState {
current_mode: Some(Mode {
width: 720,
height: 1440,
@ -619,10 +713,13 @@ pub mod test {
}),
scale: 2,
}),
Some(PixelSize {
scale_factor: 2,
pixels: 420,
}),
Some((
PixelSize {
scale_factor: 2,
pixels: 420,
},
ArrangementKind::Base,
)),
);
}
}