From 287e8517707656b104a89181d645dfae3673c3a8 Mon Sep 17 00:00:00 2001 From: Dorota Czaplejewicz Date: Thu, 3 Dec 2020 18:42:24 +0000 Subject: [PATCH 01/10] layout: Latch keys when clicked twice Third click unlatches. No actual UI indication. --- src/layout.rs | 45 +++++++++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/src/layout.rs b/src/layout.rs index 03b20039..c45e428d 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -672,6 +672,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: bool, + // 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? @@ -718,6 +724,7 @@ impl Layout { Layout { kind, current_view: "base".to_owned(), + view_latched: false, views: data.views, keymaps: data.keymaps, pressed_keys: HashSet::new(), @@ -818,6 +825,8 @@ impl Layout { } out } + + } mod procedures { @@ -1026,24 +1035,35 @@ mod seat { Action::Submit { text: _, keys: _ } | Action::Erase => { - unstick_locks(layout).apply(); + if layout.view_latched { + unstick_locks(layout).apply(); + } submission.handle_release(KeyState::get_id(rckey), time); }, Action::SetView(view) => { + layout.view_latched = false; 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() + let locked = key.action.is_locked(&layout.current_view); + let (gets_latched, new_view) = match (layout.view_latched, locked) { + // Was unlocked, now make locked but latched. (false) + // OR (true) + // Layout is latched for reason other than this button. + (_, false) => (true, Some(lock.clone())), + // Was latched, now only locked. + (true, true) => (false, None), + // Was locked, now make unlocked. + (false, true) => (false, Some(unlock.clone())), + }; + layout.view_latched = gets_latched; + if let Some(view) = new_view { + unstick_locks(layout) + // It doesn't matter what the resulting view should be, + // it's getting changed anyway. + .choose_view(view) + .apply() + } }, Action::ApplyModifier(modifier) => { // FIXME: key id is unneeded with stateless locks @@ -1193,6 +1213,7 @@ mod test { ]); let layout = Layout { current_view: String::new(), + view_latched: false, keymaps: Vec::new(), kind: ArrangementKind::Base, pressed_keys: HashSet::new(), From 3b06eadef5b0c7d5435225fabf28a760a1d6af7d Mon Sep 17 00:00:00 2001 From: Dorota Czaplejewicz Date: Thu, 3 Dec 2020 19:35:48 +0000 Subject: [PATCH 02/10] layout: Add stateless view switching Tested, but not yet plugged in. --- src/layout.rs | 300 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 236 insertions(+), 64 deletions(-) diff --git a/src/layout.rs b/src/layout.rs index c45e428d..4bc6a3e3 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -552,6 +552,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)>, @@ -825,10 +826,141 @@ 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, + ); + + self.view_latched = new_latched; + match transition { + ViewTransition::UnlatchAll => unstick_locks(self).apply(), + ViewTransition::ChangeTo(view) => { + self.current_view = view.into(); + }, + ViewTransition::NoChange => {}, + }; + } + + /// 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, + is_latched: bool, + ) -> (ViewTransition<'a>, bool) { + match action { + Action::Submit { text: _, keys: _ } + | Action::Erase + | Action::ApplyModifier(_) + => { + let t = match is_latched { + true => ViewTransition::UnlatchAll, + false => ViewTransition::NoChange, + }; + (t, false) + }, + Action::SetView(view) => (ViewTransition::ChangeTo(view), false), + Action::LockView { lock, unlock } => { + use self::ViewTransition as VT; + let locked = action.is_locked(current_view); + match (locked, is_latched) { + // Was unlocked, now make locked but latched. (false) + // OR (true) + // Layout is latched for reason other than this button. + (false, _) => (VT::ChangeTo(lock), true), + // Was latched, now only locked. + (true, true) => (VT::NoChange, false), + // Was locked, now make unlocked. + (true, false) => (VT::ChangeTo(unlock), false), + } + }, + _ => (ViewTransition::NoChange, is_latched), + } + } } +#[derive(Debug, PartialEq)] +enum ViewTransition<'a> { + ChangeTo(&'a str), + UnlatchAll, + NoChange, +} + +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, + } +} + + mod procedures { use super::*; @@ -907,67 +1039,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, @@ -1027,6 +1098,12 @@ mod seat { }; let action = key.action.clone(); + let (view_transition, latch_state) = Layout::process_action_for_view( + &action, + &layout.current_view, + layout.view_latched, + ); + // update let key = key.into_released(); @@ -1120,14 +1197,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>, @@ -1141,6 +1224,95 @@ mod test { }) } + #[test] + fn latch_lock_unlock() { + let action = Action::LockView { + lock: "lock".into(), + unlock: "unlock".into(), + }; + + assert_eq!( + Layout::process_action_for_view(&action, "unlock", false), + (ViewTransition::ChangeTo("lock"), true), + ); + + assert_eq!( + Layout::process_action_for_view(&action, "lock", true), + (ViewTransition::NoChange, false), + ); + + assert_eq!( + Layout::process_action_for_view(&action, "lock", false), + (ViewTransition::ChangeTo("unlock"), false), + ); + } + + #[test] + fn latch_pop_layout() { + let switch = Action::LockView { + lock: "locked".into(), + unlock: "base".into(), + }; + + 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: false, + 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 check_centering() { // A B From 8ab6997b215e5a13ac9a93d4c832e30e83091adf Mon Sep 17 00:00:00 2001 From: Dorota Czaplejewicz Date: Thu, 3 Dec 2020 19:44:26 +0000 Subject: [PATCH 03/10] layout: Plug in stateless view switching --- src/action.rs | 2 +- src/layout.rs | 48 ++++++------------------------------------------ 2 files changed, 7 insertions(+), 43 deletions(-) diff --git a/src/action.rs b/src/action.rs index 693dd1c5..be42fe59 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, diff --git a/src/layout.rs b/src/layout.rs index 4bc6a3e3..b84ddd6b 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -841,7 +841,7 @@ impl Layout { match transition { ViewTransition::UnlatchAll => unstick_locks(self).apply(), ViewTransition::ChangeTo(view) => { - self.current_view = view.into(); + try_set_view(self, view.into()); }, ViewTransition::NoChange => {}, }; @@ -915,12 +915,6 @@ struct ViewChange<'a> { } 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); @@ -1098,54 +1092,22 @@ mod seat { }; let action = key.action.clone(); - let (view_transition, latch_state) = Layout::process_action_for_view( - &action, - &layout.current_view, - layout.view_latched, - ); + 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 => { - if layout.view_latched { - unstick_locks(layout).apply(); - } submission.handle_release(KeyState::get_id(rckey), time); }, - Action::SetView(view) => { - layout.view_latched = false; - try_set_view(layout, view) - }, - Action::LockView { lock, unlock } => { - let locked = key.action.is_locked(&layout.current_view); - let (gets_latched, new_view) = match (layout.view_latched, locked) { - // Was unlocked, now make locked but latched. (false) - // OR (true) - // Layout is latched for reason other than this button. - (_, false) => (true, Some(lock.clone())), - // Was latched, now only locked. - (true, true) => (false, None), - // Was locked, now make unlocked. - (false, true) => (false, Some(unlock.clone())), - }; - layout.view_latched = gets_latched; - if let Some(view) = new_view { - unstick_locks(layout) - // It doesn't matter what the resulting view should be, - // it's getting changed anyway. - .choose_view(view) - .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, @@ -1180,6 +1142,8 @@ mod seat { } } }, + // Other keys are handled in view switcher before. + _ => {} }; let pointer = ::util::Pointer(rckey.clone()); From e36c4e597fbeea538f1315dd5b1246ca6f80eb07 Mon Sep 17 00:00:00 2001 From: Dorota Czaplejewicz Date: Thu, 3 Dec 2020 19:56:48 +0000 Subject: [PATCH 04/10] layout: Remove the little abomination of view change promise It didn't make anything more testable due to being tightly coupled to Layout. With the last place needing the curent form abolished, it's no longer needed. No attempt to make it more stateless and unit-testable was made though. --- src/layout.rs | 39 ++++++++++++--------------------------- 1 file changed, 12 insertions(+), 27 deletions(-) diff --git a/src/layout.rs b/src/layout.rs index b84ddd6b..bc05b07e 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -837,14 +837,13 @@ impl Layout { self.view_latched, ); - self.view_latched = new_latched; match transition { - ViewTransition::UnlatchAll => unstick_locks(self).apply(), - ViewTransition::ChangeTo(view) => { - try_set_view(self, view.into()); - }, + ViewTransition::UnlatchAll => unstick_locks(self), + ViewTransition::ChangeTo(view) => try_set_view(self, view.into()), ViewTransition::NoChange => {}, }; + + self.view_latched = new_latched; } /// Last bool is new latch state. @@ -907,31 +906,18 @@ fn try_set_view(layout: &mut Layout, view_name: String) { ); } -/// 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 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. +/// from one of the currently latched 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 { +fn unstick_locks(layout: &mut Layout) { + if !layout.view_latched { + return; + } + let mut new_view = None; for key in layout.get_locked_keys().clone() { let key: &Rc> = key.borrow(); @@ -948,9 +934,8 @@ fn unstick_locks(layout: &mut Layout) -> ViewChange { }; } - ViewChange { - layout, - view_name: new_view, + if let Some(name) = new_view { + try_set_view(layout, name); } } From 8f6252064813ae7573d9c0ae69b1c657bd4afc2b Mon Sep 17 00:00:00 2001 From: Dorota Czaplejewicz Date: Fri, 4 Dec 2020 09:30:51 +0000 Subject: [PATCH 05/10] =?UTF-8?q?view:=20=C4=84to-unlatching=20when=20mult?= =?UTF-8?q?iple=20latching=20buttons=20pressed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Best seen in the PL layout, where to get to Ą, two buttons must be latched: Capitals, and then Accents. --- src/layout.rs | 196 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 135 insertions(+), 61 deletions(-) diff --git a/src/layout.rs b/src/layout.rs index bc05b07e..aa05de77 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -666,6 +666,13 @@ pub struct Margins { pub right: f64, } +#[derive(Clone, Debug, PartialEq)] +enum LatchedState { + /// Holds view to return to. + FromView(String), + Not, +} + // TODO: split into sth like // Arrangement (views) + details (keymap) + State (keys) /// State of the UI, contains the backend as well @@ -677,7 +684,7 @@ pub struct Layout { // If current view is latched, // clicking any button that emits an action (erase, submit, set modifier) // will cause lock buttons to unlatch. - view_latched: bool, + view_latched: LatchedState, // Views own the actual buttons which have state // Maybe they should own UI only, @@ -725,7 +732,7 @@ impl Layout { Layout { kind, current_view: "base".to_owned(), - view_latched: false, + view_latched: LatchedState::Not, views: data.views, keymaps: data.keymaps, pressed_keys: HashSet::new(), @@ -834,11 +841,11 @@ impl Layout { let (transition, new_latched) = Layout::process_action_for_view( action, &self.current_view, - self.view_latched, + &self.view_latched, ); match transition { - ViewTransition::UnlatchAll => unstick_locks(self), + ViewTransition::UnlatchAll => self.unstick_locks(), ViewTransition::ChangeTo(view) => try_set_view(self, view.into()), ViewTransition::NoChange => {}, }; @@ -846,6 +853,22 @@ impl Layout { 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. @@ -858,35 +881,46 @@ impl Layout { fn process_action_for_view<'a>( action: &'a Action, current_view: &str, - is_latched: bool, - ) -> (ViewTransition<'a>, bool) { + latch: &LatchedState, + ) -> (ViewTransition<'a>, LatchedState) { match action { Action::Submit { text: _, keys: _ } | Action::Erase | Action::ApplyModifier(_) => { - let t = match is_latched { - true => ViewTransition::UnlatchAll, - false => ViewTransition::NoChange, + let t = match latch { + LatchedState::FromView(_) => ViewTransition::UnlatchAll, + LatchedState::Not => ViewTransition::NoChange, }; - (t, false) + (t, LatchedState::Not) }, - Action::SetView(view) => (ViewTransition::ChangeTo(view), false), + Action::SetView(view) => ( + ViewTransition::ChangeTo(view), + LatchedState::Not, + ), Action::LockView { lock, unlock } => { use self::ViewTransition as VT; let locked = action.is_locked(current_view); - match (locked, is_latched) { - // Was unlocked, now make locked but latched. (false) - // OR (true) + match (locked, latch) { + // Was unlocked, now make locked but latched. + (false, LatchedState::Not) => ( + VT::ChangeTo(lock), + LatchedState::FromView(current_view.into()), + ), // Layout is latched for reason other than this button. - (false, _) => (VT::ChangeTo(lock), true), + (false, LatchedState::FromView(view)) => ( + VT::ChangeTo(lock), + LatchedState::FromView(view.clone()), + ), // Was latched, now only locked. - (true, true) => (VT::NoChange, false), + (true, LatchedState::FromView(_)) + => (VT::NoChange, LatchedState::Not), // Was locked, now make unlocked. - (true, false) => (VT::ChangeTo(unlock), false), + (true, LatchedState::Not) + => (VT::ChangeTo(unlock), LatchedState::Not), } }, - _ => (ViewTransition::NoChange, is_latched), + _ => (ViewTransition::NoChange, latch.clone()), } } } @@ -898,47 +932,14 @@ enum ViewTransition<'a> { NoChange, } -fn try_set_view(layout: &mut Layout, view_name: String) { - layout.set_view(view_name.clone()) +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), ); } -/// Find all impermanent view changes and undo them in an arbitrary order. -/// The final view is the "unlock" view -/// from one of the currently latched 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. -fn unstick_locks(layout: &mut Layout) { - if !layout.view_latched { - return; - } - - 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, - ), - }; - } - - if let Some(name) = new_view { - try_set_view(layout, name); - } -} - mod procedures { use super::*; @@ -1181,18 +1182,23 @@ mod test { }; assert_eq!( - Layout::process_action_for_view(&action, "unlock", false), - (ViewTransition::ChangeTo("lock"), true), + 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", true), - (ViewTransition::NoChange, false), + Layout::process_action_for_view(&action, "lock", &LatchedState::FromView("unlock".into())), + (ViewTransition::NoChange, LatchedState::Not), ); assert_eq!( - Layout::process_action_for_view(&action, "lock", false), - (ViewTransition::ChangeTo("unlock"), false), + 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), ); } @@ -1227,7 +1233,7 @@ mod test { let mut layout = Layout { current_view: "base".into(), - view_latched: false, + view_latched: LatchedState::Not, keymaps: Vec::new(), kind: ArrangementKind::Base, pressed_keys: HashSet::new(), @@ -1262,6 +1268,74 @@ mod test { assert_eq!(&layout.current_view, "base"); } + #[test] + fn latch_twopop_layout() { + let switch = Action::LockView { + lock: "locked".into(), + unlock: "base".into(), + }; + + let switch_again = Action::LockView { + lock: "ĄĘ".into(), + unlock: "locked".into(), + }; + + 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 @@ -1334,7 +1408,7 @@ mod test { ]); let layout = Layout { current_view: String::new(), - view_latched: false, + view_latched: LatchedState::Not, keymaps: Vec::new(), kind: ArrangementKind::Base, pressed_keys: HashSet::new(), From 9522d4e302c83f666b0a806149570ccbb8ff29c2 Mon Sep 17 00:00:00 2001 From: Dorota Czaplejewicz Date: Fri, 4 Dec 2020 14:07:02 +0000 Subject: [PATCH 06/10] renderer: Bring button drawing closer to Rust --- eek/eek-renderer.c | 46 ++++++++++-------- src/drawing.rs | 115 ++++++++++++++++++++++++++++++++++++++++----- src/layout.h | 6 --- src/layout.rs | 62 ++++-------------------- 4 files changed, 138 insertions(+), 91 deletions(-) diff --git a/eek/eek-renderer.c b/eek/eek-renderer.c index 143c553e..d7dcdf43 100644 --- a/eek/eek-renderer.c +++ b/eek/eek-renderer.c @@ -18,8 +18,6 @@ * 02110-1301 USA */ -#include "config.h" - #include #include #include @@ -60,21 +58,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 +102,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, + uint64_t pressed, + uint64_t locked) { 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,14 +131,17 @@ 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"); } 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, + uint64_t locked) { // 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); @@ -333,6 +336,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/src/drawing.rs b/src/drawing.rs index 5a0f91ad..432ff938 100644 --- a/src/drawing.rs +++ b/src/drawing.rs @@ -5,18 +5,21 @@ use std::cell::RefCell; use ::action::Action; use ::keyboard; -use ::layout::{ Button, Layout }; -use ::layout::c::{ EekGtkKeyboard, Point }; +use ::layout::{ Button, Label, Layout }; +use ::layout::c::{ Bounds, EekGtkKeyboard, Point }; use ::submission::Submission; use glib::translate::FromGlibPtrNone; use gtk::WidgetExt; +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,18 +27,45 @@ 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); + + #[no_mangle] 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, pressed: u64, locked: u64, + ) -> GtkStyleContext; + + #[allow(improper_ctypes)] + pub fn eek_put_style_context_for_button( + ctx: GtkStyleContext, + outline_name: *const c_char, + locked: u64, ); } @@ -109,16 +139,75 @@ 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: bool, + operation: F, +) -> R { + let outline_name_c = button.outline_name.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, 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 as u64, + ) + }; + + r } pub fn queue_redraw(keyboard: EekGtkKeyboard) { diff --git a/src/layout.h b/src/layout.h index 53318bc4..086b4097 100644 --- a/src/layout.h +++ b/src/layout.h @@ -26,12 +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( diff --git a/src/layout.rs b/src/layout.rs index aa05de77..aa2f4198 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::ops::{ Add, Sub }; @@ -162,57 +160,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" @@ -485,6 +432,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 { From c6cc58fd8e1dd1c6e0e23f7904900a1cfd38055f Mon Sep 17 00:00:00 2001 From: Dorota Czaplejewicz Date: Fri, 4 Dec 2020 15:34:31 +0000 Subject: [PATCH 07/10] ffi: Eliminate squeek_button and squeek_row --- eek/eek-renderer.c | 4 ---- eek/eek-types.h | 8 -------- src/layout.h | 1 - src/layout.rs | 9 +-------- src/popover.h | 9 --------- 5 files changed, 1 insertion(+), 30 deletions(-) delete mode 100644 src/popover.h diff --git a/eek/eek-renderer.c b/eek/eek-renderer.c index d7dcdf43..e5f1cd52 100644 --- a/eek/eek-renderer.c +++ b/eek/eek-renderer.c @@ -31,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, 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/layout.h b/src/layout.h index 086b4097..519ecbc6 100644 --- a/src/layout.h +++ b/src/layout.h @@ -26,7 +26,6 @@ struct squeek_layout_state { struct squeek_layout; -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 aa2f4198..20984523 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -41,7 +41,7 @@ pub mod c { use super::*; use gtk_sys; - use std::os::raw::{ c_char, c_void }; + use std::os::raw::c_void; use std::ops::{ Add, Sub }; @@ -161,13 +161,6 @@ pub mod c { // 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_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. #[no_mangle] 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 From 676a2b60ac586679aa072f2a2f5927b3e18826f2 Mon Sep 17 00:00:00 2001 From: Dorota Czaplejewicz Date: Fri, 4 Dec 2020 16:45:00 +0000 Subject: [PATCH 08/10] layout: Make it possible to opt out of latching per-key --- src/action.rs | 7 ++-- src/data.rs | 10 ++++-- src/layout.rs | 89 +++++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 93 insertions(+), 13 deletions(-) diff --git a/src/action.rs b/src/action.rs index be42fe59..862dcbf1 100644 --- a/src/action.rs +++ b/src/action.rs @@ -29,6 +29,9 @@ 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, }, /// Hold this modifier for as long as the button is pressed ApplyModifier(Modifier), @@ -48,14 +51,14 @@ 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: _ } => lock == view_name, _ => 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: _ } => lock == view_name, _ => false, } } diff --git a/src/data.rs b/src/data.rs index ab0c4d5a..60e86db6 100644 --- a/src/data.rs +++ b/src/data.rs @@ -266,7 +266,11 @@ 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(rename="set_view")] SetView(String), #[serde(rename="show_prefs")] @@ -577,7 +581,8 @@ fn create_action( ) ), SubmitData::Action(Action::Locking { - lock_view, unlock_view + lock_view, unlock_view, + pops, }) => ::action::Action::LockView { lock: filter_view_name( name, @@ -591,6 +596,7 @@ fn create_action( &view_names, warning_handler, ), + latches: pops.unwrap_or(true), }, SubmitData::Action( Action::ShowPrefs diff --git a/src/layout.rs b/src/layout.rs index 20984523..7ca0adb5 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -830,14 +830,14 @@ impl Layout { fn process_action_for_view<'a>( action: &'a Action, current_view: &str, - latch: &LatchedState, + latched: &LatchedState, ) -> (ViewTransition<'a>, LatchedState) { match action { Action::Submit { text: _, keys: _ } | Action::Erase | Action::ApplyModifier(_) => { - let t = match latch { + let t = match latched { LatchedState::FromView(_) => ViewTransition::UnlatchAll, LatchedState::Not => ViewTransition::NoChange, }; @@ -847,29 +847,32 @@ impl Layout { ViewTransition::ChangeTo(view), LatchedState::Not, ), - Action::LockView { lock, unlock } => { + Action::LockView { lock, unlock, latches } => { use self::ViewTransition as VT; let locked = action.is_locked(current_view); - match (locked, latch) { + match (locked, latched, latches) { // Was unlocked, now make locked but latched. - (false, LatchedState::Not) => ( + (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)) => ( + (false, LatchedState::FromView(view), true) => ( VT::ChangeTo(lock), LatchedState::FromView(view.clone()), ), // Was latched, now only locked. - (true, LatchedState::FromView(_)) + (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, LatchedState::Not) + (true, _, _) => (VT::ChangeTo(unlock), LatchedState::Not), } }, - _ => (ViewTransition::NoChange, latch.clone()), + _ => (ViewTransition::NoChange, latched.clone()), } } } @@ -1128,6 +1131,7 @@ mod test { let action = Action::LockView { lock: "lock".into(), unlock: "unlock".into(), + latches: true, }; assert_eq!( @@ -1156,6 +1160,7 @@ mod test { let switch = Action::LockView { lock: "locked".into(), unlock: "base".into(), + latches: true, }; let submit = Action::Erase; @@ -1217,16 +1222,82 @@ mod test { assert_eq!(&layout.current_view, "base"); } + #[test] + fn reverse_unlatch_layout() { + let switch = Action::LockView { + lock: "locked".into(), + unlock: "base".into(), + latches: true, + }; + + let unswitch = Action::LockView { + lock: "locked".into(), + unlock: "unlocked".into(), + latches: false, + }; + + 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, }; let switch_again = Action::LockView { lock: "ĄĘ".into(), unlock: "locked".into(), + latches: true, }; let submit = Action::Erase; From 7b1755a489b06de7379c5167514f2b3a8b69bb02 Mon Sep 17 00:00:00 2001 From: Dorota Czaplejewicz Date: Fri, 4 Dec 2020 15:27:43 +0000 Subject: [PATCH 09/10] renderer: Mark latched buttons differently than locked There are some hacks here in the form of an extra field "appears_locked_from", which can be used to hint that the user should see the button as locked. Without it, there's some confusion on user side regarding buttons that change states unprompted. --- data/keyboards/pl.yaml | 4 ++ eek/eek-renderer.c | 14 +++--- src/action.rs | 16 ++++++- src/data.rs | 4 ++ src/drawing.rs | 105 ++++++++++++++++++++++++++++++++++------- src/layout.rs | 22 ++++++++- 6 files changed, 137 insertions(+), 28 deletions(-) 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/eek/eek-renderer.c b/eek/eek-renderer.c index e5f1cd52..4f64b6e8 100644 --- a/eek/eek-renderer.c +++ b/eek/eek-renderer.c @@ -111,8 +111,8 @@ GtkStyleContext * eek_get_style_context_for_button (EekRenderer *self, const char *name, const char *outline_name, - uint64_t pressed, - uint64_t locked) + 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 @@ -127,8 +127,8 @@ eek_get_style_context_for_button (EekRenderer *self, (pressed) or normal. */ gtk_style_context_set_state(ctx, pressed ? GTK_STATE_FLAG_ACTIVE : GTK_STATE_FLAG_NORMAL); - 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; @@ -137,12 +137,12 @@ eek_get_style_context_for_button (EekRenderer *self, /// Interface for Rust. void eek_put_style_context_for_button(GtkStyleContext *ctx, const char *outline_name, - uint64_t locked) { + 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); } } diff --git a/src/action.rs b/src/action.rs index 862dcbf1..46d34940 100644 --- a/src/action.rs +++ b/src/action.rs @@ -32,6 +32,8 @@ pub enum Action { /// 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), @@ -51,14 +53,24 @@ pub enum Action { impl Action { pub fn is_locked(&self, view_name: &str) -> bool { match self { - Action::LockView { lock, unlock: _, latches: _ } => 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: _, latches: _ } => 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 60e86db6..1fc8939c 100644 --- a/src/data.rs +++ b/src/data.rs @@ -270,6 +270,8 @@ enum Action { lock_view: String, unlock_view: String, pops: Option, + #[serde(default)] + looks_locked_from: Vec, }, #[serde(rename="set_view")] SetView(String), @@ -583,6 +585,7 @@ fn create_action( SubmitData::Action(Action::Locking { lock_view, unlock_view, pops, + looks_locked_from, }) => ::action::Action::LockView { lock: filter_view_name( name, @@ -597,6 +600,7 @@ fn create_action( 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 432ff938..7dfc3cd7 100644 --- a/src/drawing.rs +++ b/src/drawing.rs @@ -3,15 +3,16 @@ use cairo; use std::cell::RefCell; -use ::action::Action; +use ::action::{ Action, Modifier }; use ::keyboard; -use ::layout::{ Button, Label, Layout }; +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; @@ -57,15 +58,15 @@ mod c { 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: u64, + locked_class: *const c_char, ); } @@ -85,13 +86,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, @@ -117,20 +121,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); @@ -182,18 +221,27 @@ fn with_button_context R>( renderer: c::EekRenderer, button: &Button, pressed: keyboard::PressType, - locked: bool, + 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, button.name.as_ptr(), outline_name_c, + locked_class_c, pressed as u64, - locked as u64, ) }; @@ -203,7 +251,7 @@ fn with_button_context R>( c::eek_put_style_context_for_button( ctx, outline_name_c, - locked as u64, + locked_class_c, ) }; @@ -214,3 +262,26 @@ 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.rs b/src/layout.rs index 7ca0adb5..8d7c1f21 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -616,12 +616,18 @@ pub struct Margins { } #[derive(Clone, Debug, PartialEq)] -enum LatchedState { +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 @@ -707,6 +713,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( @@ -847,7 +859,7 @@ impl Layout { ViewTransition::ChangeTo(view), LatchedState::Not, ), - Action::LockView { lock, unlock, latches } => { + Action::LockView { lock, unlock, latches, looks_locked_from: _ } => { use self::ViewTransition as VT; let locked = action.is_locked(current_view); match (locked, latched, latches) { @@ -1132,6 +1144,7 @@ mod test { lock: "lock".into(), unlock: "unlock".into(), latches: true, + looks_locked_from: vec![], }; assert_eq!( @@ -1161,6 +1174,7 @@ mod test { lock: "locked".into(), unlock: "base".into(), latches: true, + looks_locked_from: vec![], }; let submit = Action::Erase; @@ -1228,12 +1242,14 @@ mod test { 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; @@ -1292,12 +1308,14 @@ mod test { 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; From 89b1f51ed5c0cb0f136774ef957c537ccfa41890 Mon Sep 17 00:00:00 2001 From: Dorota Czaplejewicz Date: Fri, 4 Dec 2020 16:27:49 +0000 Subject: [PATCH 10/10] appearance: Colour latched/locked according to design --- data/style-Adwaita:dark.css | 7 ++++++- data/style.css | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) 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; }