Merge branch 'latch' into 'master'
Latch views See merge request Librem5/squeekboard!416
This commit is contained in:
@ -52,6 +52,8 @@ buttons:
|
||||
locking:
|
||||
lock_view: "upper_accents"
|
||||
unlock_view: "accents"
|
||||
looks_locked_from:
|
||||
- "upper"
|
||||
outline: "altline"
|
||||
icon: "key-shift"
|
||||
BackSpace:
|
||||
@ -94,6 +96,8 @@ buttons:
|
||||
locking:
|
||||
lock_view: "upper_accents"
|
||||
unlock_view: "upper"
|
||||
looks_locked_from:
|
||||
- "accents"
|
||||
outline: "altline"
|
||||
label: "ĄĘ"
|
||||
period:
|
||||
|
||||
@ -31,11 +31,16 @@ sq_button.wide {
|
||||
border-color: #3e3a44;
|
||||
}
|
||||
|
||||
sq_button.locked {
|
||||
sq_button.latched {
|
||||
background: #ffffff;
|
||||
color: #2b292f;
|
||||
}
|
||||
|
||||
sq_button.locked {
|
||||
background: #ffffff;
|
||||
color: #1c71d8;
|
||||
}
|
||||
|
||||
sq_button.action {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
@ -34,11 +34,16 @@ sq_button.wide {
|
||||
border-color: @borders; /* #3e3a44; */
|
||||
}
|
||||
|
||||
sq_button.locked {
|
||||
sq_button.latched {
|
||||
background: @theme_fg_color; /*#ffffff;*/
|
||||
color: @theme_bg_color; /*#2b292f;*/
|
||||
}
|
||||
|
||||
sq_button.locked {
|
||||
background: @theme_fg_color; /*#ffffff;*/
|
||||
color: mix(@theme_selected_bg_color, @theme_bg_color, 0.4); /*#2b292f;*/
|
||||
}
|
||||
|
||||
sq_button.action {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
@ -18,8 +18,6 @@
|
||||
* 02110-1301 USA
|
||||
*/
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
#include <gdk-pixbuf/gdk-pixbuf.h>
|
||||
@ -33,10 +31,6 @@
|
||||
static void render_button_label (cairo_t *cr, GtkStyleContext *ctx,
|
||||
const gchar *label, EekBounds bounds);
|
||||
|
||||
void eek_render_button (EekRenderer *self,
|
||||
cairo_t *cr, const struct squeek_button *button,
|
||||
gboolean pressed, gboolean locked);
|
||||
|
||||
static void
|
||||
render_outline (cairo_t *cr,
|
||||
GtkStyleContext *ctx,
|
||||
@ -60,21 +54,21 @@ render_outline (cairo_t *cr,
|
||||
position.x, position.y, position.width, position.height);
|
||||
}
|
||||
|
||||
static void render_button_in_context(gint scale_factor,
|
||||
/// Rust interface
|
||||
void eek_render_button_in_context(uint32_t scale_factor,
|
||||
cairo_t *cr,
|
||||
GtkStyleContext *ctx,
|
||||
const struct squeek_button *button) {
|
||||
EekBounds bounds,
|
||||
const char *icon_name,
|
||||
const gchar *label) {
|
||||
/* blank background */
|
||||
cairo_set_source_rgba (cr, 0.0, 0.0, 0.0, 0.0);
|
||||
cairo_paint (cr);
|
||||
|
||||
EekBounds bounds = squeek_button_get_bounds(button);
|
||||
render_outline (cr, ctx, bounds);
|
||||
cairo_paint (cr);
|
||||
|
||||
/* render icon (if any) */
|
||||
const char *icon_name = squeek_button_get_icon_name(button);
|
||||
|
||||
if (icon_name) {
|
||||
cairo_surface_t *icon_surface =
|
||||
eek_renderer_get_icon_surface (icon_name, 16, scale_factor);
|
||||
@ -104,25 +98,27 @@ static void render_button_in_context(gint scale_factor,
|
||||
}
|
||||
}
|
||||
|
||||
const gchar *label = squeek_button_get_label(button);
|
||||
if (label) {
|
||||
render_button_label (cr, ctx, label, squeek_button_get_bounds(button));
|
||||
render_button_label (cr, ctx, label, bounds);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
eek_render_button (EekRenderer *self,
|
||||
cairo_t *cr,
|
||||
const struct squeek_button *button,
|
||||
gboolean pressed,
|
||||
gboolean locked)
|
||||
/// Prepare context for drawing the button.
|
||||
/// The context MUST be released using the corresponing "put" procedure
|
||||
/// before drawing the next button.
|
||||
/// Interface for Rust.
|
||||
GtkStyleContext *
|
||||
eek_get_style_context_for_button (EekRenderer *self,
|
||||
const char *name,
|
||||
const char *outline_name,
|
||||
const char *locked_class,
|
||||
uint64_t pressed)
|
||||
{
|
||||
GtkStyleContext *ctx = self->button_context;
|
||||
/* Set the name of the button on the widget path, using the name obtained
|
||||
from the button's symbol. */
|
||||
g_autoptr (GtkWidgetPath) path = NULL;
|
||||
path = gtk_widget_path_copy (gtk_style_context_get_path (ctx));
|
||||
const char *name = squeek_button_get_name(button);
|
||||
gtk_widget_path_iter_set_name (path, -1, name);
|
||||
|
||||
/* Update the style context with the updated widget path. */
|
||||
@ -131,19 +127,22 @@ eek_render_button (EekRenderer *self,
|
||||
(pressed) or normal. */
|
||||
gtk_style_context_set_state(ctx,
|
||||
pressed ? GTK_STATE_FLAG_ACTIVE : GTK_STATE_FLAG_NORMAL);
|
||||
const char *outline_name = squeek_button_get_outline_name(button);
|
||||
if (locked) {
|
||||
gtk_style_context_add_class(ctx, "locked");
|
||||
if (locked_class) {
|
||||
gtk_style_context_add_class(ctx, locked_class);
|
||||
}
|
||||
gtk_style_context_add_class(ctx, outline_name);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
render_button_in_context(self->scale_factor, cr, ctx, button);
|
||||
|
||||
/// Interface for Rust.
|
||||
void eek_put_style_context_for_button(GtkStyleContext *ctx,
|
||||
const char *outline_name,
|
||||
const char *locked_class) {
|
||||
// Save and restore functions don't work if gtk_render_* was used in between
|
||||
gtk_style_context_set_state(ctx, GTK_STATE_FLAG_NORMAL);
|
||||
gtk_style_context_remove_class(ctx, outline_name);
|
||||
if (locked) {
|
||||
gtk_style_context_remove_class(ctx, "locked");
|
||||
if (locked_class) {
|
||||
gtk_style_context_remove_class(ctx, locked_class);
|
||||
}
|
||||
}
|
||||
|
||||
@ -328,6 +327,11 @@ eek_renderer_set_scale_factor (EekRenderer *renderer, gint scale)
|
||||
renderer->scale_factor = scale;
|
||||
}
|
||||
|
||||
/// Rust interface.
|
||||
uint32_t eek_renderer_get_scale_factor(EekRenderer *renderer) {
|
||||
return renderer->scale_factor;
|
||||
}
|
||||
|
||||
cairo_surface_t *
|
||||
eek_renderer_get_icon_surface (const gchar *icon_name,
|
||||
gint size,
|
||||
|
||||
@ -90,13 +90,5 @@ struct transformation {
|
||||
gdouble scale;
|
||||
};
|
||||
|
||||
struct squeek_button;
|
||||
struct squeek_row;
|
||||
|
||||
/// Represents the path to the button within a view
|
||||
struct button_place {
|
||||
const struct squeek_row *row;
|
||||
const struct squeek_button *button;
|
||||
};
|
||||
G_END_DECLS
|
||||
#endif /* EEK_TYPES_H */
|
||||
|
||||
@ -10,7 +10,7 @@ pub struct KeySym(pub String);
|
||||
type View = String;
|
||||
|
||||
/// Use to send modified keypresses
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Modifier {
|
||||
/// Control and Alt are the only modifiers
|
||||
/// which doesn't interfere with levels,
|
||||
@ -30,6 +30,11 @@ pub enum Action {
|
||||
lock: View,
|
||||
/// When unlocked by pressing it or emitting a key
|
||||
unlock: View,
|
||||
/// Whether key has a latched state
|
||||
/// that pops when another key is pressed.
|
||||
latches: bool,
|
||||
/// Should take on *locked* appearance whenever latch comes back to those views.
|
||||
looks_locked_from: Vec<View>,
|
||||
},
|
||||
/// Hold this modifier for as long as the button is pressed
|
||||
ApplyModifier(Modifier),
|
||||
@ -49,14 +54,24 @@ pub enum Action {
|
||||
impl Action {
|
||||
pub fn is_locked(&self, view_name: &str) -> bool {
|
||||
match self {
|
||||
Action::LockView { lock, unlock: _ } => lock == view_name,
|
||||
Action::LockView { lock, unlock: _, latches: _, looks_locked_from: _ } => lock == view_name,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
pub fn has_locked_appearance_from(&self, locked_view_name: &str) -> bool {
|
||||
match self {
|
||||
Action::LockView { lock: _, unlock: _, latches: _, looks_locked_from } => {
|
||||
looks_locked_from.iter()
|
||||
.find(|view| locked_view_name == view.as_str())
|
||||
.is_some()
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
pub fn is_active(&self, view_name: &str) -> bool {
|
||||
match self {
|
||||
Action::SetView(view) => view == view_name,
|
||||
Action::LockView { lock, unlock: _ } => lock == view_name,
|
||||
Action::LockView { lock, unlock: _, latches: _, looks_locked_from: _ } => lock == view_name,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
14
src/data.rs
14
src/data.rs
@ -289,7 +289,13 @@ struct ButtonMeta {
|
||||
#[serde(deny_unknown_fields)]
|
||||
enum Action {
|
||||
#[serde(rename="locking")]
|
||||
Locking { lock_view: String, unlock_view: String },
|
||||
Locking {
|
||||
lock_view: String,
|
||||
unlock_view: String,
|
||||
pops: Option<bool>,
|
||||
#[serde(default)]
|
||||
looks_locked_from: Vec<String>,
|
||||
},
|
||||
#[serde(rename="set_view")]
|
||||
SetView(String),
|
||||
#[serde(rename="show_prefs")]
|
||||
@ -600,7 +606,9 @@ fn create_action<H: logging::Handler>(
|
||||
)
|
||||
),
|
||||
SubmitData::Action(Action::Locking {
|
||||
lock_view, unlock_view
|
||||
lock_view, unlock_view,
|
||||
pops,
|
||||
looks_locked_from,
|
||||
}) => ::action::Action::LockView {
|
||||
lock: filter_view_name(
|
||||
name,
|
||||
@ -614,6 +622,8 @@ fn create_action<H: logging::Handler>(
|
||||
&view_names,
|
||||
warning_handler,
|
||||
),
|
||||
latches: pops.unwrap_or(true),
|
||||
looks_locked_from,
|
||||
},
|
||||
SubmitData::Action(
|
||||
Action::ShowPrefs
|
||||
|
||||
212
src/drawing.rs
212
src/drawing.rs
@ -3,20 +3,24 @@
|
||||
use cairo;
|
||||
use std::cell::RefCell;
|
||||
|
||||
use ::action::Action;
|
||||
use ::action::{ Action, Modifier };
|
||||
use ::keyboard;
|
||||
use ::layout::{ Button, Layout };
|
||||
use ::layout::c::{ EekGtkKeyboard, Point };
|
||||
use ::layout::{ Button, Label, LatchedState, Layout };
|
||||
use ::layout::c::{ Bounds, EekGtkKeyboard, Point };
|
||||
use ::submission::Submission;
|
||||
|
||||
use glib::translate::FromGlibPtrNone;
|
||||
use gtk::WidgetExt;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::ffi::CStr;
|
||||
use std::ptr;
|
||||
|
||||
mod c {
|
||||
use super::*;
|
||||
|
||||
use cairo_sys;
|
||||
use std::os::raw::c_void;
|
||||
use std::os::raw::{ c_char, c_void };
|
||||
|
||||
// This is constructed only in C, no need for warnings
|
||||
#[allow(dead_code)]
|
||||
@ -24,17 +28,44 @@ mod c {
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct EekRenderer(*const c_void);
|
||||
|
||||
// This is constructed only in C, no need for warnings
|
||||
/// Just don't clone this for no reason.
|
||||
#[allow(dead_code)]
|
||||
#[repr(transparent)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct GtkStyleContext(*const c_void);
|
||||
|
||||
|
||||
extern "C" {
|
||||
// Button and View inside CButtonPlace are safe to pass to C
|
||||
// as long as they don't outlive the call
|
||||
// and nothing dereferences them
|
||||
#[allow(improper_ctypes)]
|
||||
pub fn eek_render_button(
|
||||
pub fn eek_renderer_get_scale_factor(
|
||||
renderer: EekRenderer,
|
||||
) -> u32;
|
||||
|
||||
#[allow(improper_ctypes)]
|
||||
pub fn eek_render_button_in_context(
|
||||
scale_factor: u32,
|
||||
cr: *mut cairo_sys::cairo_t,
|
||||
button: *const Button,
|
||||
ctx: GtkStyleContext,
|
||||
bounds: Bounds,
|
||||
icon_name: *const c_char,
|
||||
label: *const c_char,
|
||||
);
|
||||
|
||||
#[allow(improper_ctypes)]
|
||||
pub fn eek_get_style_context_for_button(
|
||||
renderer: EekRenderer,
|
||||
name: *const c_char,
|
||||
outline_name: *const c_char,
|
||||
locked_class: *const c_char,
|
||||
pressed: u64,
|
||||
locked: u64,
|
||||
) -> GtkStyleContext;
|
||||
|
||||
#[allow(improper_ctypes)]
|
||||
pub fn eek_put_style_context_for_button(
|
||||
ctx: GtkStyleContext,
|
||||
outline_name: *const c_char,
|
||||
locked_class: *const c_char,
|
||||
);
|
||||
}
|
||||
|
||||
@ -54,13 +85,16 @@ mod c {
|
||||
|
||||
layout.foreach_visible_button(|offset, button| {
|
||||
let state = RefCell::borrow(&button.state).clone();
|
||||
let active_mod = match &state.action {
|
||||
Action::ApplyModifier(m) => active_modifiers.contains(m),
|
||||
_ => false,
|
||||
};
|
||||
let locked = state.action.is_active(&layout.current_view)
|
||||
| active_mod;
|
||||
if state.pressed == keyboard::PressType::Pressed || locked {
|
||||
|
||||
let locked = LockedStyle::from_action(
|
||||
&state.action,
|
||||
&active_modifiers,
|
||||
layout.get_view_latched(),
|
||||
&layout.current_view,
|
||||
);
|
||||
if state.pressed == keyboard::PressType::Pressed
|
||||
|| locked != LockedStyle::Free
|
||||
{
|
||||
render_button_at_position(
|
||||
renderer, &cr,
|
||||
offset,
|
||||
@ -86,20 +120,55 @@ mod c {
|
||||
renderer, &cr,
|
||||
offset,
|
||||
button.as_ref(),
|
||||
keyboard::PressType::Released, false,
|
||||
keyboard::PressType::Released,
|
||||
LockedStyle::Free,
|
||||
);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
enum LockedStyle {
|
||||
Free,
|
||||
Latched,
|
||||
Locked,
|
||||
}
|
||||
|
||||
impl LockedStyle {
|
||||
fn from_action(
|
||||
action: &Action,
|
||||
mods: &HashSet<Modifier>,
|
||||
latched_view: &LatchedState,
|
||||
current_view: &str,
|
||||
) -> LockedStyle {
|
||||
let active_mod = match action {
|
||||
Action::ApplyModifier(m) => mods.contains(m),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let active_view = action.is_active(current_view);
|
||||
let latched_button = match latched_view {
|
||||
LatchedState::Not => false,
|
||||
LatchedState::FromView(view) => !action.has_locked_appearance_from(view),
|
||||
};
|
||||
match (active_mod, active_view, latched_button) {
|
||||
// Modifiers don't latch.
|
||||
(true, _, _) => LockedStyle::Locked,
|
||||
(false, true, false) => LockedStyle::Locked,
|
||||
(false, true, true) => LockedStyle::Latched,
|
||||
_ => LockedStyle::Free,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders a button at a position (button's own bounds ignored)
|
||||
pub fn render_button_at_position(
|
||||
fn render_button_at_position(
|
||||
renderer: c::EekRenderer,
|
||||
cr: &cairo::Context,
|
||||
position: Point,
|
||||
button: &Button,
|
||||
pressed: keyboard::PressType,
|
||||
locked: bool,
|
||||
locked: LockedStyle,
|
||||
) {
|
||||
cr.save();
|
||||
cr.translate(position.x, position.y);
|
||||
@ -108,19 +177,110 @@ pub fn render_button_at_position(
|
||||
button.size.width, button.size.height
|
||||
);
|
||||
cr.clip();
|
||||
unsafe {
|
||||
c::eek_render_button(
|
||||
|
||||
let scale_factor = unsafe {
|
||||
c::eek_renderer_get_scale_factor(renderer)
|
||||
};
|
||||
let bounds = button.get_bounds();
|
||||
let (label_c, icon_name_c) = match &button.label {
|
||||
Label::Text(text) => (text.as_ptr(), ptr::null()),
|
||||
Label::IconName(name) => {
|
||||
let l = unsafe {
|
||||
// CStr doesn't allocate anything, so it only points to
|
||||
// the 'static str, avoiding a memory leak
|
||||
CStr::from_bytes_with_nul_unchecked(b"icon\0")
|
||||
};
|
||||
(l.as_ptr(), name.as_ptr())
|
||||
},
|
||||
};
|
||||
|
||||
with_button_context(
|
||||
renderer,
|
||||
button,
|
||||
pressed,
|
||||
locked,
|
||||
|ctx| unsafe {
|
||||
// TODO: split into separate procedures:
|
||||
// draw outline, draw label, draw icon.
|
||||
c::eek_render_button_in_context(
|
||||
scale_factor,
|
||||
cairo::Context::to_raw_none(&cr),
|
||||
*ctx,
|
||||
bounds,
|
||||
icon_name_c,
|
||||
label_c,
|
||||
)
|
||||
}
|
||||
);
|
||||
|
||||
cr.restore();
|
||||
}
|
||||
|
||||
fn with_button_context<R, F: FnOnce(&c::GtkStyleContext) -> R>(
|
||||
renderer: c::EekRenderer,
|
||||
button: &Button,
|
||||
pressed: keyboard::PressType,
|
||||
locked: LockedStyle,
|
||||
operation: F,
|
||||
) -> R {
|
||||
let outline_name_c = button.outline_name.as_ptr();
|
||||
let locked_class_c = match locked {
|
||||
LockedStyle::Free => ptr::null(),
|
||||
LockedStyle::Locked => unsafe {
|
||||
CStr::from_bytes_with_nul_unchecked(b"locked\0").as_ptr()
|
||||
},
|
||||
LockedStyle::Latched => unsafe {
|
||||
CStr::from_bytes_with_nul_unchecked(b"latched\0").as_ptr()
|
||||
},
|
||||
};
|
||||
|
||||
let ctx = unsafe {
|
||||
c::eek_get_style_context_for_button(
|
||||
renderer,
|
||||
cairo::Context::to_raw_none(&cr),
|
||||
button as *const Button,
|
||||
button.name.as_ptr(),
|
||||
outline_name_c,
|
||||
locked_class_c,
|
||||
pressed as u64,
|
||||
locked as u64,
|
||||
)
|
||||
};
|
||||
cr.restore();
|
||||
|
||||
let r = operation(&ctx);
|
||||
|
||||
unsafe {
|
||||
c::eek_put_style_context_for_button(
|
||||
ctx,
|
||||
outline_name_c,
|
||||
locked_class_c,
|
||||
)
|
||||
};
|
||||
|
||||
r
|
||||
}
|
||||
|
||||
pub fn queue_redraw(keyboard: EekGtkKeyboard) {
|
||||
let widget = unsafe { gtk::Widget::from_glib_none(keyboard.0) };
|
||||
widget.queue_draw();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_exit_only() {
|
||||
assert_eq!(
|
||||
LockedStyle::from_action(
|
||||
&Action::LockView {
|
||||
lock: "ab".into(),
|
||||
unlock: "a".into(),
|
||||
latches: true,
|
||||
looks_locked_from: vec!["b".into()],
|
||||
},
|
||||
&HashSet::new(),
|
||||
&LatchedState::FromView("b".into()),
|
||||
"ab",
|
||||
),
|
||||
LockedStyle::Locked,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,13 +26,6 @@ struct squeek_layout_state {
|
||||
|
||||
struct squeek_layout;
|
||||
|
||||
EekBounds squeek_button_get_bounds(const struct squeek_button*);
|
||||
const char *squeek_button_get_label(const struct squeek_button*);
|
||||
const char *squeek_button_get_icon_name(const struct squeek_button*);
|
||||
const char *squeek_button_get_name(const struct squeek_button*);
|
||||
const char *squeek_button_get_outline_name(const struct squeek_button*);
|
||||
|
||||
void squeek_button_print(const struct squeek_button* button);
|
||||
|
||||
struct transformation squeek_layout_calculate_transformation(
|
||||
const struct squeek_layout *layout,
|
||||
|
||||
540
src/layout.rs
540
src/layout.rs
@ -41,9 +41,7 @@ pub mod c {
|
||||
use super::*;
|
||||
|
||||
use gtk_sys;
|
||||
use std::ffi::CStr;
|
||||
use std::os::raw::{ c_char, c_void };
|
||||
use std::ptr;
|
||||
use std::os::raw::c_void;
|
||||
|
||||
use std::ops::{ Add, Sub };
|
||||
|
||||
@ -161,64 +159,6 @@ pub mod c {
|
||||
pub struct LevelKeyboard(*const c_void);
|
||||
|
||||
// The following defined in Rust. TODO: wrap naked pointers to Rust data inside RefCells to prevent multiple writers
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C"
|
||||
fn squeek_button_get_bounds(button: *const ::layout::Button) -> Bounds {
|
||||
let button = unsafe { &*button };
|
||||
Bounds {
|
||||
x: 0.0, y: 0.0,
|
||||
width: button.size.width, height: button.size.height
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C"
|
||||
fn squeek_button_get_label(
|
||||
button: *const ::layout::Button
|
||||
) -> *const c_char {
|
||||
let button = unsafe { &*button };
|
||||
match &button.label {
|
||||
Label::Text(text) => text.as_ptr(),
|
||||
// returning static strings to C is a bit cumbersome
|
||||
Label::IconName(_) => unsafe {
|
||||
// CStr doesn't allocate anything, so it only points to
|
||||
// the 'static str, avoiding a memory leak
|
||||
CStr::from_bytes_with_nul_unchecked(b"icon\0")
|
||||
}.as_ptr(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C"
|
||||
fn squeek_button_get_icon_name(button: *const Button) -> *const c_char {
|
||||
let button = unsafe { &*button };
|
||||
match &button.label {
|
||||
Label::Text(_) => ptr::null(),
|
||||
Label::IconName(name) => name.as_ptr(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C"
|
||||
fn squeek_button_get_name(button: *const Button) -> *const c_char {
|
||||
let button = unsafe { &*button };
|
||||
button.name.as_ptr()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C"
|
||||
fn squeek_button_get_outline_name(button: *const Button) -> *const c_char {
|
||||
let button = unsafe { &*button };
|
||||
button.outline_name.as_ptr()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C"
|
||||
fn squeek_button_print(button: *const ::layout::Button) {
|
||||
let button = unsafe { &*button };
|
||||
println!("{:?}", button);
|
||||
}
|
||||
|
||||
/// Positions the layout contents within the available space.
|
||||
/// The origin of the transformation is the point inside the margins.
|
||||
@ -484,6 +424,15 @@ pub struct Button {
|
||||
pub state: Rc<RefCell<KeyState>>,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
pub fn get_bounds(&self) -> c::Bounds {
|
||||
c::Bounds {
|
||||
x: 0.0, y: 0.0,
|
||||
width: self.size.width, height: self.size.height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The graphical representation of a row of buttons
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Row {
|
||||
@ -551,6 +500,7 @@ pub struct Spacing {
|
||||
pub button: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct View {
|
||||
/// Rows together with their offsets from the top left
|
||||
rows: Vec<(c::Point, Row)>,
|
||||
@ -664,6 +614,19 @@ pub struct Margins {
|
||||
pub right: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum LatchedState {
|
||||
/// Holds view to return to.
|
||||
FromView(String),
|
||||
Not,
|
||||
}
|
||||
|
||||
impl LatchedState {
|
||||
pub fn is_latched(&self) -> bool {
|
||||
self != &LatchedState::Not
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: split into sth like
|
||||
// Arrangement (views) + details (keymap) + State (keys)
|
||||
/// State of the UI, contains the backend as well
|
||||
@ -671,6 +634,12 @@ pub struct Layout {
|
||||
pub margins: Margins,
|
||||
pub kind: ArrangementKind,
|
||||
pub current_view: String,
|
||||
|
||||
// If current view is latched,
|
||||
// clicking any button that emits an action (erase, submit, set modifier)
|
||||
// will cause lock buttons to unlatch.
|
||||
view_latched: LatchedState,
|
||||
|
||||
// Views own the actual buttons which have state
|
||||
// Maybe they should own UI only,
|
||||
// and keys should be owned by a dedicated non-UI-State?
|
||||
@ -717,6 +686,7 @@ impl Layout {
|
||||
Layout {
|
||||
kind,
|
||||
current_view: "base".to_owned(),
|
||||
view_latched: LatchedState::Not,
|
||||
views: data.views,
|
||||
keymaps: data.keymaps,
|
||||
pressed_keys: HashSet::new(),
|
||||
@ -742,6 +712,12 @@ impl Layout {
|
||||
}
|
||||
}
|
||||
|
||||
// Layout is passed around mutably,
|
||||
// so better keep the field away from direct access.
|
||||
pub fn get_view_latched(&self) -> &LatchedState {
|
||||
&self.view_latched
|
||||
}
|
||||
|
||||
/// Calculates size without margins
|
||||
fn calculate_inner_size(&self) -> Size {
|
||||
View::calculate_super_size(
|
||||
@ -817,8 +793,117 @@ impl Layout {
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn apply_view_transition(
|
||||
&mut self,
|
||||
action: &Action,
|
||||
) {
|
||||
let (transition, new_latched) = Layout::process_action_for_view(
|
||||
action,
|
||||
&self.current_view,
|
||||
&self.view_latched,
|
||||
);
|
||||
|
||||
match transition {
|
||||
ViewTransition::UnlatchAll => self.unstick_locks(),
|
||||
ViewTransition::ChangeTo(view) => try_set_view(self, view.into()),
|
||||
ViewTransition::NoChange => {},
|
||||
};
|
||||
|
||||
self.view_latched = new_latched;
|
||||
}
|
||||
|
||||
/// Unlatch all latched keys,
|
||||
/// so that the new view is the one before first press.
|
||||
fn unstick_locks(&mut self) {
|
||||
if let LatchedState::FromView(name) = self.view_latched.clone() {
|
||||
match self.set_view(name.clone()) {
|
||||
Ok(_) => { self.view_latched = LatchedState::Not; }
|
||||
Err(e) => log_print!(
|
||||
logging::Level::Bug,
|
||||
"Bad view {}, can't unlatch ({:?})",
|
||||
name,
|
||||
e,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Last bool is new latch state.
|
||||
/// It doesn't make sense when the result carries UnlatchAll,
|
||||
/// but let's not be picky.
|
||||
///
|
||||
/// Although the state is not defined at the keys
|
||||
/// (it's in the relationship between view and action),
|
||||
/// keys go through the following stages when clicked repeatedly:
|
||||
/// unlocked+unlatched -> locked+latched -> locked+unlatched
|
||||
/// -> unlocked+unlatched
|
||||
fn process_action_for_view<'a>(
|
||||
action: &'a Action,
|
||||
current_view: &str,
|
||||
latched: &LatchedState,
|
||||
) -> (ViewTransition<'a>, LatchedState) {
|
||||
match action {
|
||||
Action::Submit { text: _, keys: _ }
|
||||
| Action::Erase
|
||||
| Action::ApplyModifier(_)
|
||||
=> {
|
||||
let t = match latched {
|
||||
LatchedState::FromView(_) => ViewTransition::UnlatchAll,
|
||||
LatchedState::Not => ViewTransition::NoChange,
|
||||
};
|
||||
(t, LatchedState::Not)
|
||||
},
|
||||
Action::SetView(view) => (
|
||||
ViewTransition::ChangeTo(view),
|
||||
LatchedState::Not,
|
||||
),
|
||||
Action::LockView { lock, unlock, latches, looks_locked_from: _ } => {
|
||||
use self::ViewTransition as VT;
|
||||
let locked = action.is_locked(current_view);
|
||||
match (locked, latched, latches) {
|
||||
// Was unlocked, now make locked but latched.
|
||||
(false, LatchedState::Not, true) => (
|
||||
VT::ChangeTo(lock),
|
||||
LatchedState::FromView(current_view.into()),
|
||||
),
|
||||
// Layout is latched for reason other than this button.
|
||||
(false, LatchedState::FromView(view), true) => (
|
||||
VT::ChangeTo(lock),
|
||||
LatchedState::FromView(view.clone()),
|
||||
),
|
||||
// Was latched, now only locked.
|
||||
(true, LatchedState::FromView(_), true)
|
||||
=> (VT::NoChange, LatchedState::Not),
|
||||
// Was unlocked, can't latch so now make fully locked.
|
||||
(false, _, false)
|
||||
=> (VT::ChangeTo(lock), LatchedState::Not),
|
||||
// Was locked, now make unlocked.
|
||||
(true, _, _)
|
||||
=> (VT::ChangeTo(unlock), LatchedState::Not),
|
||||
}
|
||||
},
|
||||
_ => (ViewTransition::NoChange, latched.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum ViewTransition<'a> {
|
||||
ChangeTo(&'a str),
|
||||
UnlatchAll,
|
||||
NoChange,
|
||||
}
|
||||
|
||||
fn try_set_view(layout: &mut Layout, view_name: &str) {
|
||||
layout.set_view(view_name.into())
|
||||
.or_print(
|
||||
logging::Problem::Bug,
|
||||
&format!("Bad view {}, ignoring", view_name),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
mod procedures {
|
||||
use super::*;
|
||||
|
||||
@ -897,67 +982,6 @@ pub struct UIBackend {
|
||||
mod seat {
|
||||
use super::*;
|
||||
|
||||
fn try_set_view(layout: &mut Layout, view_name: String) {
|
||||
layout.set_view(view_name.clone())
|
||||
.or_print(
|
||||
logging::Problem::Bug,
|
||||
&format!("Bad view {}, ignoring", view_name),
|
||||
);
|
||||
}
|
||||
|
||||
/// A vessel holding an obligation to switch view.
|
||||
/// Use with #[must_use]
|
||||
struct ViewChange<'a> {
|
||||
layout: &'a mut Layout,
|
||||
view_name: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> ViewChange<'a> {
|
||||
fn choose_view(self, view_name: String) -> ViewChange<'a> {
|
||||
ViewChange {
|
||||
view_name: Some(view_name),
|
||||
..self
|
||||
}
|
||||
}
|
||||
fn apply(self) {
|
||||
if let Some(name) = self.view_name {
|
||||
try_set_view(self.layout, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find all impermanent view changes and undo them in an arbitrary order.
|
||||
/// Return an obligation to actually switch the view.
|
||||
/// The final view is the "unlock" view
|
||||
/// from one of the currently stuck keys.
|
||||
// As long as only one stuck button is used, this should be fine.
|
||||
// This is guaranteed because pressing a lock button unlocks all others.
|
||||
// TODO: Make some broader guarantee about the resulting view,
|
||||
// e.g. by maintaining a stack of stuck keys.
|
||||
#[must_use]
|
||||
fn unstick_locks(layout: &mut Layout) -> ViewChange {
|
||||
let mut new_view = None;
|
||||
for key in layout.get_locked_keys().clone() {
|
||||
let key: &Rc<RefCell<KeyState>> = key.borrow();
|
||||
let key = RefCell::borrow(key);
|
||||
match &key.action {
|
||||
Action::LockView { lock: _, unlock: view } => {
|
||||
new_view = Some(view.clone());
|
||||
},
|
||||
a => log_print!(
|
||||
logging::Level::Bug,
|
||||
"Non-locking action {:?} was found inside locked keys",
|
||||
a,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
ViewChange {
|
||||
layout,
|
||||
view_name: new_view,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_press_key(
|
||||
layout: &mut Layout,
|
||||
submission: &mut Submission,
|
||||
@ -1017,37 +1041,22 @@ mod seat {
|
||||
};
|
||||
let action = key.action.clone();
|
||||
|
||||
layout.apply_view_transition(&action);
|
||||
|
||||
// update
|
||||
let key = key.into_released();
|
||||
|
||||
// process changes
|
||||
// process non-view switching
|
||||
match action {
|
||||
Action::Submit { text: _, keys: _ }
|
||||
| Action::Erase
|
||||
=> {
|
||||
unstick_locks(layout).apply();
|
||||
submission.handle_release(KeyState::get_id(rckey), time);
|
||||
},
|
||||
Action::SetView(view) => {
|
||||
try_set_view(layout, view)
|
||||
},
|
||||
Action::LockView { lock, unlock } => {
|
||||
let gets_locked = !key.action.is_locked(&layout.current_view);
|
||||
unstick_locks(layout)
|
||||
// It doesn't matter what the resulting view should be,
|
||||
// it's getting changed anyway.
|
||||
.choose_view(
|
||||
match gets_locked {
|
||||
true => lock.clone(),
|
||||
false => unlock.clone(),
|
||||
}
|
||||
)
|
||||
.apply()
|
||||
},
|
||||
Action::ApplyModifier(modifier) => {
|
||||
// FIXME: key id is unneeded with stateless locks
|
||||
let key_id = KeyState::get_id(rckey);
|
||||
let gets_locked = !submission.is_modifier_active(modifier.clone());
|
||||
let gets_locked = !submission.is_modifier_active(modifier);
|
||||
match gets_locked {
|
||||
true => submission.handle_add_modifier(
|
||||
key_id,
|
||||
@ -1082,6 +1091,8 @@ mod seat {
|
||||
}
|
||||
}
|
||||
},
|
||||
// Other keys are handled in view switcher before.
|
||||
_ => {}
|
||||
};
|
||||
|
||||
let pointer = ::util::Pointer(rckey.clone());
|
||||
@ -1099,14 +1110,20 @@ mod test {
|
||||
use std::ffi::CString;
|
||||
use ::keyboard::PressType;
|
||||
|
||||
pub fn make_state() -> Rc<RefCell<::keyboard::KeyState>> {
|
||||
pub fn make_state_with_action(action: Action)
|
||||
-> Rc<RefCell<::keyboard::KeyState>>
|
||||
{
|
||||
Rc::new(RefCell::new(::keyboard::KeyState {
|
||||
pressed: PressType::Released,
|
||||
keycodes: Vec::new(),
|
||||
action: Action::SetView("default".into()),
|
||||
action,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn make_state() -> Rc<RefCell<::keyboard::KeyState>> {
|
||||
make_state_with_action(Action::SetView("default".into()))
|
||||
}
|
||||
|
||||
pub fn make_button_with_state(
|
||||
name: String,
|
||||
state: Rc<RefCell<::keyboard::KeyState>>,
|
||||
@ -1120,6 +1137,242 @@ mod test {
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn latch_lock_unlock() {
|
||||
let action = Action::LockView {
|
||||
lock: "lock".into(),
|
||||
unlock: "unlock".into(),
|
||||
latches: true,
|
||||
looks_locked_from: vec![],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
Layout::process_action_for_view(&action, "unlock", &LatchedState::Not),
|
||||
(ViewTransition::ChangeTo("lock"), LatchedState::FromView("unlock".into())),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Layout::process_action_for_view(&action, "lock", &LatchedState::FromView("unlock".into())),
|
||||
(ViewTransition::NoChange, LatchedState::Not),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Layout::process_action_for_view(&action, "lock", &LatchedState::Not),
|
||||
(ViewTransition::ChangeTo("unlock"), LatchedState::Not),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Layout::process_action_for_view(&Action::Erase, "lock", &LatchedState::FromView("base".into())),
|
||||
(ViewTransition::UnlatchAll, LatchedState::Not),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn latch_pop_layout() {
|
||||
let switch = Action::LockView {
|
||||
lock: "locked".into(),
|
||||
unlock: "base".into(),
|
||||
latches: true,
|
||||
looks_locked_from: vec![],
|
||||
};
|
||||
|
||||
let submit = Action::Erase;
|
||||
|
||||
let view = View::new(vec![(
|
||||
0.0,
|
||||
Row::new(vec![
|
||||
(
|
||||
0.0,
|
||||
make_button_with_state(
|
||||
"switch".into(),
|
||||
make_state_with_action(switch.clone())
|
||||
),
|
||||
),
|
||||
(
|
||||
1.0,
|
||||
make_button_with_state(
|
||||
"submit".into(),
|
||||
make_state_with_action(submit.clone())
|
||||
),
|
||||
),
|
||||
]),
|
||||
)]);
|
||||
|
||||
let mut layout = Layout {
|
||||
current_view: "base".into(),
|
||||
view_latched: LatchedState::Not,
|
||||
keymaps: Vec::new(),
|
||||
kind: ArrangementKind::Base,
|
||||
pressed_keys: HashSet::new(),
|
||||
margins: Margins {
|
||||
top: 0.0,
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
bottom: 0.0,
|
||||
},
|
||||
views: hashmap! {
|
||||
// Both can use the same structure.
|
||||
// Switching doesn't depend on the view shape
|
||||
// as long as the switching button is present.
|
||||
"base".into() => (c::Point { x: 0.0, y: 0.0 }, view.clone()),
|
||||
"locked".into() => (c::Point { x: 0.0, y: 0.0 }, view),
|
||||
},
|
||||
};
|
||||
|
||||
// Basic cycle
|
||||
layout.apply_view_transition(&switch);
|
||||
assert_eq!(&layout.current_view, "locked");
|
||||
layout.apply_view_transition(&switch);
|
||||
assert_eq!(&layout.current_view, "locked");
|
||||
layout.apply_view_transition(&submit);
|
||||
assert_eq!(&layout.current_view, "locked");
|
||||
layout.apply_view_transition(&switch);
|
||||
assert_eq!(&layout.current_view, "base");
|
||||
layout.apply_view_transition(&switch);
|
||||
// Unlatch
|
||||
assert_eq!(&layout.current_view, "locked");
|
||||
layout.apply_view_transition(&submit);
|
||||
assert_eq!(&layout.current_view, "base");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reverse_unlatch_layout() {
|
||||
let switch = Action::LockView {
|
||||
lock: "locked".into(),
|
||||
unlock: "base".into(),
|
||||
latches: true,
|
||||
looks_locked_from: vec![],
|
||||
};
|
||||
|
||||
let unswitch = Action::LockView {
|
||||
lock: "locked".into(),
|
||||
unlock: "unlocked".into(),
|
||||
latches: false,
|
||||
looks_locked_from: vec![],
|
||||
};
|
||||
|
||||
let submit = Action::Erase;
|
||||
|
||||
let view = View::new(vec![(
|
||||
0.0,
|
||||
Row::new(vec![
|
||||
(
|
||||
0.0,
|
||||
make_button_with_state(
|
||||
"switch".into(),
|
||||
make_state_with_action(switch.clone())
|
||||
),
|
||||
),
|
||||
(
|
||||
1.0,
|
||||
make_button_with_state(
|
||||
"submit".into(),
|
||||
make_state_with_action(submit.clone())
|
||||
),
|
||||
),
|
||||
]),
|
||||
)]);
|
||||
|
||||
let mut layout = Layout {
|
||||
current_view: "base".into(),
|
||||
view_latched: LatchedState::Not,
|
||||
keymaps: Vec::new(),
|
||||
kind: ArrangementKind::Base,
|
||||
pressed_keys: HashSet::new(),
|
||||
margins: Margins {
|
||||
top: 0.0,
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
bottom: 0.0,
|
||||
},
|
||||
views: hashmap! {
|
||||
// Both can use the same structure.
|
||||
// Switching doesn't depend on the view shape
|
||||
// as long as the switching button is present.
|
||||
"base".into() => (c::Point { x: 0.0, y: 0.0 }, view.clone()),
|
||||
"locked".into() => (c::Point { x: 0.0, y: 0.0 }, view.clone()),
|
||||
"unlocked".into() => (c::Point { x: 0.0, y: 0.0 }, view),
|
||||
},
|
||||
};
|
||||
|
||||
layout.apply_view_transition(&switch);
|
||||
assert_eq!(&layout.current_view, "locked");
|
||||
layout.apply_view_transition(&unswitch);
|
||||
assert_eq!(&layout.current_view, "unlocked");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn latch_twopop_layout() {
|
||||
let switch = Action::LockView {
|
||||
lock: "locked".into(),
|
||||
unlock: "base".into(),
|
||||
latches: true,
|
||||
looks_locked_from: vec![],
|
||||
};
|
||||
|
||||
let switch_again = Action::LockView {
|
||||
lock: "ĄĘ".into(),
|
||||
unlock: "locked".into(),
|
||||
latches: true,
|
||||
looks_locked_from: vec![],
|
||||
};
|
||||
|
||||
let submit = Action::Erase;
|
||||
|
||||
let view = View::new(vec![(
|
||||
0.0,
|
||||
Row::new(vec![
|
||||
(
|
||||
0.0,
|
||||
make_button_with_state(
|
||||
"switch".into(),
|
||||
make_state_with_action(switch.clone())
|
||||
),
|
||||
),
|
||||
(
|
||||
1.0,
|
||||
make_button_with_state(
|
||||
"submit".into(),
|
||||
make_state_with_action(submit.clone())
|
||||
),
|
||||
),
|
||||
]),
|
||||
)]);
|
||||
|
||||
let mut layout = Layout {
|
||||
current_view: "base".into(),
|
||||
view_latched: LatchedState::Not,
|
||||
keymaps: Vec::new(),
|
||||
kind: ArrangementKind::Base,
|
||||
pressed_keys: HashSet::new(),
|
||||
margins: Margins {
|
||||
top: 0.0,
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
bottom: 0.0,
|
||||
},
|
||||
views: hashmap! {
|
||||
// All can use the same structure.
|
||||
// Switching doesn't depend on the view shape
|
||||
// as long as the switching button is present.
|
||||
"base".into() => (c::Point { x: 0.0, y: 0.0 }, view.clone()),
|
||||
"locked".into() => (c::Point { x: 0.0, y: 0.0 }, view.clone()),
|
||||
"ĄĘ".into() => (c::Point { x: 0.0, y: 0.0 }, view),
|
||||
},
|
||||
};
|
||||
|
||||
// Latch twice, then Ąto-unlatch across 2 levels
|
||||
layout.apply_view_transition(&switch);
|
||||
println!("{:?}", layout.view_latched);
|
||||
assert_eq!(&layout.current_view, "locked");
|
||||
layout.apply_view_transition(&switch_again);
|
||||
println!("{:?}", layout.view_latched);
|
||||
assert_eq!(&layout.current_view, "ĄĘ");
|
||||
layout.apply_view_transition(&submit);
|
||||
println!("{:?}", layout.view_latched);
|
||||
assert_eq!(&layout.current_view, "base");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn check_centering() {
|
||||
// A B
|
||||
@ -1192,6 +1445,7 @@ mod test {
|
||||
]);
|
||||
let layout = Layout {
|
||||
current_view: String::new(),
|
||||
view_latched: LatchedState::Not,
|
||||
keymaps: Vec::new(),
|
||||
kind: ArrangementKind::Base,
|
||||
pressed_keys: HashSet::new(),
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
#ifndef POPOVER_H__
|
||||
#define POPOVER_H__
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
#include "eek/eek-keyboard.h"
|
||||
|
||||
void squeek_popover_show(GtkWidget*, struct button_place);
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user