diff --git a/data/keyboards/pl.yaml b/data/keyboards/pl.yaml index f6c45411..348ed856 100644 --- a/data/keyboards/pl.yaml +++ b/data/keyboards/pl.yaml @@ -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: diff --git a/data/style-Adwaita:dark.css b/data/style-Adwaita:dark.css index 2165543c..6b52750f 100644 --- a/data/style-Adwaita:dark.css +++ b/data/style-Adwaita:dark.css @@ -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; } diff --git a/data/style.css b/data/style.css index a010ef47..40b8ffa6 100644 --- a/data/style.css +++ b/data/style.css @@ -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; } diff --git a/eek/eek-renderer.c b/eek/eek-renderer.c index 1ae3540f..39ab68a5 100644 --- a/eek/eek-renderer.c +++ b/eek/eek-renderer.c @@ -18,8 +18,6 @@ * 02110-1301 USA */ -#include "config.h" - #include #include #include @@ -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, diff --git a/eek/eek-types.h b/eek/eek-types.h index 5d993191..8f52f716 100644 --- a/eek/eek-types.h +++ b/eek/eek-types.h @@ -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 */ diff --git a/src/action.rs b/src/action.rs index c56462f7..5b7f5fe9 100644 --- a/src/action.rs +++ b/src/action.rs @@ -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, }, /// 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, } } diff --git a/src/data.rs b/src/data.rs index ecc63fe4..3bb8e5ce 100644 --- a/src/data.rs +++ b/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, + #[serde(default)] + looks_locked_from: Vec, + }, #[serde(rename="set_view")] SetView(String), #[serde(rename="show_prefs")] @@ -600,7 +606,9 @@ fn create_action( ) ), 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( &view_names, warning_handler, ), + latches: pops.unwrap_or(true), + looks_locked_from, }, SubmitData::Action( Action::ShowPrefs diff --git a/src/drawing.rs b/src/drawing.rs index 58210268..312af9ce 100644 --- a/src/drawing.rs +++ b/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, + 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>( + 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, + ); + } +} diff --git a/src/layout.h b/src/layout.h index 53318bc4..519ecbc6 100644 --- a/src/layout.h +++ b/src/layout.h @@ -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, diff --git a/src/layout.rs b/src/layout.rs index c464a522..413795f0 100644 --- a/src/layout.rs +++ b/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>, } +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, - } - - 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> = 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> { + pub fn make_state_with_action(action: Action) + -> Rc> + { Rc::new(RefCell::new(::keyboard::KeyState { pressed: PressType::Released, keycodes: Vec::new(), - action: Action::SetView("default".into()), + action, })) } + pub fn make_state() -> Rc> { + make_state_with_action(Action::SetView("default".into())) + } + pub fn make_button_with_state( name: String, state: Rc>, @@ -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(), diff --git a/src/popover.h b/src/popover.h deleted file mode 100644 index 1b164e5a..00000000 --- a/src/popover.h +++ /dev/null @@ -1,9 +0,0 @@ -#ifndef POPOVER_H__ -#define POPOVER_H__ - -#include -#include "eek/eek-keyboard.h" - -void squeek_popover_show(GtkWidget*, struct button_place); - -#endif