diff --git a/src/animation.rs b/src/animation.rs index 7ea07dfa..8a602fbd 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -6,12 +6,17 @@ use std::time::Duration; +use crate::outputs::OutputId; + /// The keyboard should hide after this has elapsed to prevent flickering. pub const HIDING_TIMEOUT: Duration = Duration::from_millis(200); /// The outwardly visible state of visibility #[derive(PartialEq, Debug, Clone)] pub enum Outcome { - Visible, + Visible { + output: OutputId, + height: u32, + }, Hidden, } diff --git a/src/assert_matches.rs b/src/assert_matches.rs new file mode 100644 index 00000000..a7ef3c2d --- /dev/null +++ b/src/assert_matches.rs @@ -0,0 +1,358 @@ +/* Taken from https://github.com/murarth/assert_matches + * + * git commit: 26b8b40a12823c068a829ba475d0eccc13dfc221 + * + * assert_matches is distributed under the terms of both the MIT license and the Apache License (Version 2.0). + * +Copyright (c) 2016 Murarth + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + + */ + +//! Provides a macro, `assert_matches!`, which tests whether a value +//! matches a given pattern, causing a panic if the match fails. +//! +//! See the macro [`assert_matches!`] documentation for more information. +//! +//! Also provides a debug-only counterpart, [`debug_assert_matches!`]. +//! +//! See the macro [`debug_assert_matches!`] documentation for more information +//! about this macro. +//! +//! [`assert_matches!`]: macro.assert_matches.html +//! [`debug_assert_matches!`]: macro.debug_assert_matches.html + +#![deny(missing_docs)] +#![cfg_attr(not(test), no_std)] + +/// Asserts that an expression matches a given pattern. +/// +/// A guard expression may be supplied to add further restrictions to the +/// expected value of the expression. +/// +/// A `match` arm may be supplied to perform additional assertions or to yield +/// a value from the macro invocation. +/// +/// # Examples +/// +/// ``` +/// #[macro_use] extern crate assert_matches; +/// +/// #[derive(Debug)] +/// enum Foo { +/// A(i32), +/// B(&'static str), +/// } +/// +/// # fn main() { +/// let a = Foo::A(1); +/// +/// // Assert that `a` matches the pattern `Foo::A(_)`. +/// assert_matches!(a, Foo::A(_)); +/// +/// // Assert that `a` matches the pattern and +/// // that the contained value meets the condition `i > 0`. +/// assert_matches!(a, Foo::A(i) if i > 0); +/// +/// let b = Foo::B("foobar"); +/// +/// // Assert that `b` matches the pattern `Foo::B(_)`. +/// assert_matches!(b, Foo::B(s) => { +/// // Perform additional assertions on the variable binding `s`. +/// assert!(s.starts_with("foo")); +/// assert!(s.ends_with("bar")); +/// }); +/// +/// // Assert that `b` matches the pattern and yield the string `s`. +/// let s = assert_matches!(b, Foo::B(s) => s); +/// +/// // Perform an assertion on the value `s`. +/// assert_eq!(s, "foobar"); +/// # } +/// ``` +#[macro_export] +macro_rules! assert_matches { + ( $e:expr , $($pat:pat)|+ ) => { + match $e { + $($pat)|+ => (), + ref e => panic!("assertion failed: `{:?}` does not match `{}`", + e, stringify!($($pat)|+)) + } + }; + ( $e:expr , $($pat:pat)|+ if $cond:expr ) => { + match $e { + $($pat)|+ if $cond => (), + ref e => panic!("assertion failed: `{:?}` does not match `{}`", + e, stringify!($($pat)|+ if $cond)) + } + }; + ( $e:expr , $($pat:pat)|+ => $arm:expr ) => { + match $e { + $($pat)|+ => $arm, + ref e => panic!("assertion failed: `{:?}` does not match `{}`", + e, stringify!($($pat)|+)) + } + }; + ( $e:expr , $($pat:pat)|+ if $cond:expr => $arm:expr ) => { + match $e { + $($pat)|+ if $cond => $arm, + ref e => panic!("assertion failed: `{:?}` does not match `{}`", + e, stringify!($($pat)|+ if $cond)) + } + }; + ( $e:expr , $($pat:pat)|+ , $($arg:tt)* ) => { + match $e { + $($pat)|+ => (), + ref e => panic!("assertion failed: `{:?}` does not match `{}`: {}", + e, stringify!($($pat)|+), format_args!($($arg)*)) + } + }; + ( $e:expr , $($pat:pat)|+ if $cond:expr , $($arg:tt)* ) => { + match $e { + $($pat)|+ if $cond => (), + ref e => panic!("assertion failed: `{:?}` does not match `{}`: {}", + e, stringify!($($pat)|+ if $cond), format_args!($($arg)*)) + } + }; + ( $e:expr , $($pat:pat)|+ => $arm:expr , $($arg:tt)* ) => { + match $e { + $($pat)|+ => $arm, + ref e => panic!("assertion failed: `{:?}` does not match `{}`: {}", + e, stringify!($($pat)|+), format_args!($($arg)*)) + } + }; + ( $e:expr , $($pat:pat)|+ if $cond:expr => $arm:expr , $($arg:tt)* ) => { + match $e { + $($pat)|+ if $cond => $arm, + ref e => panic!("assertion failed: `{:?}` does not match `{}`: {}", + e, stringify!($($pat)|+ if $cond), format_args!($($arg)*)) + } + }; +} + +/// Asserts that an expression matches a given pattern. +/// +/// Unlike [`assert_matches!`], `debug_assert_matches!` statements are only enabled +/// in non-optimized builds by default. An optimized build will omit all +/// `debug_assert_matches!` statements unless `-C debug-assertions` is passed +/// to the compiler. +/// +/// See the macro [`assert_matches!`] documentation for more information. +/// +/// [`assert_matches!`]: macro.assert_matches.html +#[macro_export(local_inner_macros)] +macro_rules! debug_assert_matches { + ( $($tt:tt)* ) => { { + if _assert_matches_cfg!(debug_assertions) { + assert_matches!($($tt)*); + } + } } +} + +#[doc(hidden)] +#[macro_export] +macro_rules! _assert_matches_cfg { + ( $($tt:tt)* ) => { cfg!($($tt)*) } +} + +#[cfg(test)] +mod test { + use std::panic::{catch_unwind, UnwindSafe}; + + #[derive(Debug)] + enum Foo { + A(i32), + B(&'static str), + C(&'static str), + } + + #[test] + fn test_assert_succeed() { + let a = Foo::A(123); + + assert_matches!(a, Foo::A(_)); + assert_matches!(a, Foo::A(123)); + assert_matches!(a, Foo::A(i) if i == 123); + assert_matches!(a, Foo::A(42) | Foo::A(123)); + + let b = Foo::B("foo"); + + assert_matches!(b, Foo::B(_)); + assert_matches!(b, Foo::B("foo")); + assert_matches!(b, Foo::B(s) if s == "foo"); + assert_matches!(b, Foo::B(s) => assert_eq!(s, "foo")); + assert_matches!(b, Foo::B(s) => { assert_eq!(s, "foo"); assert!(true) }); + assert_matches!(b, Foo::B(s) if s == "foo" => assert_eq!(s, "foo")); + assert_matches!(b, Foo::B(s) if s == "foo" => { assert_eq!(s, "foo"); assert!(true) }); + + let c = Foo::C("foo"); + + assert_matches!(c, Foo::B(_) | Foo::C(_)); + assert_matches!(c, Foo::B("foo") | Foo::C("foo")); + assert_matches!(c, Foo::B(s) | Foo::C(s) if s == "foo"); + assert_matches!(c, Foo::B(s) | Foo::C(s) => assert_eq!(s, "foo")); + assert_matches!(c, Foo::B(s) | Foo::C(s) => { assert_eq!(s, "foo"); assert!(true) }); + assert_matches!(c, Foo::B(s) | Foo::C(s) if s == "foo" => assert_eq!(s, "foo")); + assert_matches!(c, Foo::B(s) | Foo::C(s) if s == "foo" => { assert_eq!(s, "foo"); assert!(true) }); + } + + #[test] + #[should_panic] + fn test_assert_panic_0() { + let a = Foo::A(123); + + assert_matches!(a, Foo::B(_)); + } + + #[test] + #[should_panic] + fn test_assert_panic_1() { + let b = Foo::B("foo"); + + assert_matches!(b, Foo::B("bar")); + } + + #[test] + #[should_panic] + fn test_assert_panic_2() { + let b = Foo::B("foo"); + + assert_matches!(b, Foo::B(s) if s == "bar"); + } + + #[test] + #[should_panic] + fn test_assert_panic_3() { + let b = Foo::B("foo"); + + assert_matches!(b, Foo::B(s) => assert_eq!(s, "bar")); + } + + #[test] + #[should_panic] + fn test_assert_panic_4() { + let b = Foo::B("foo"); + + assert_matches!(b, Foo::B(s) if s == "bar" => assert_eq!(s, "foo")); + } + + #[test] + #[should_panic] + fn test_assert_panic_5() { + let b = Foo::B("foo"); + + assert_matches!(b, Foo::B(s) if s == "foo" => assert_eq!(s, "bar")); + } + + #[test] + #[should_panic] + fn test_assert_panic_6() { + let b = Foo::B("foo"); + + assert_matches!(b, Foo::B(s) if s == "foo" => { assert_eq!(s, "foo"); assert!(false) }); + } + + #[test] + fn test_assert_no_move() { + let b = &mut Foo::A(0); + assert_matches!(*b, Foo::A(0)); + } + + #[test] + fn assert_with_message() { + let a = Foo::A(0); + + assert_matches!(a, Foo::A(_), "o noes"); + assert_matches!(a, Foo::A(n) if n == 0, "o noes"); + assert_matches!(a, Foo::A(n) => assert_eq!(n, 0), "o noes"); + assert_matches!(a, Foo::A(n) => { assert_eq!(n, 0); assert!(n < 1) }, "o noes"); + assert_matches!(a, Foo::A(n) if n == 0 => assert_eq!(n, 0), "o noes"); + assert_matches!(a, Foo::A(n) if n == 0 => { assert_eq!(n, 0); assert!(n < 1) }, "o noes"); + assert_matches!(a, Foo::A(_), "o noes {:?}", a); + assert_matches!(a, Foo::A(n) if n == 0, "o noes {:?}", a); + assert_matches!(a, Foo::A(n) => assert_eq!(n, 0), "o noes {:?}", a); + assert_matches!(a, Foo::A(n) => { assert_eq!(n, 0); assert!(n < 1) }, "o noes {:?}", a); + assert_matches!(a, Foo::A(_), "o noes {value:?}", value=a); + assert_matches!(a, Foo::A(n) if n == 0, "o noes {value:?}", value=a); + assert_matches!(a, Foo::A(n) => assert_eq!(n, 0), "o noes {value:?}", value=a); + assert_matches!(a, Foo::A(n) => { assert_eq!(n, 0); assert!(n < 1) }, "o noes {value:?}", value=a); + assert_matches!(a, Foo::A(n) if n == 0 => assert_eq!(n, 0), "o noes {value:?}", value=a); + } + + fn panic_message(f: F) -> String + where F: FnOnce() + UnwindSafe { + let err = catch_unwind(f) + .expect_err("function did not panic"); + + *err.downcast::() + .expect("function panicked with non-String value") + } + + #[test] + fn test_panic_message() { + let a = Foo::A(1); + + // expr, pat + assert_eq!(panic_message(|| { + assert_matches!(a, Foo::B(_)); + }), r#"assertion failed: `A(1)` does not match `Foo::B(_)`"#); + + // expr, pat if cond + assert_eq!(panic_message(|| { + assert_matches!(a, Foo::B(s) if s == "foo"); + }), r#"assertion failed: `A(1)` does not match `Foo::B(s) if s == "foo"`"#); + + // expr, pat => arm + assert_eq!(panic_message(|| { + assert_matches!(a, Foo::B(_) => {}); + }), r#"assertion failed: `A(1)` does not match `Foo::B(_)`"#); + + // expr, pat if cond => arm + assert_eq!(panic_message(|| { + assert_matches!(a, Foo::B(s) if s == "foo" => {}); + }), r#"assertion failed: `A(1)` does not match `Foo::B(s) if s == "foo"`"#); + + // expr, pat, args + assert_eq!(panic_message(|| { + assert_matches!(a, Foo::B(_), "msg"); + }), r#"assertion failed: `A(1)` does not match `Foo::B(_)`: msg"#); + + // expr, pat if cond, args + assert_eq!(panic_message(|| { + assert_matches!(a, Foo::B(s) if s == "foo", "msg"); + }), r#"assertion failed: `A(1)` does not match `Foo::B(s) if s == "foo"`: msg"#); + + // expr, pat => arm, args + assert_eq!(panic_message(|| { + assert_matches!(a, Foo::B(_) => {}, "msg"); + }), r#"assertion failed: `A(1)` does not match `Foo::B(_)`: msg"#); + + // expr, pat if cond => arm, args + assert_eq!(panic_message(|| { + assert_matches!(a, Foo::B(s) if s == "foo" => {}, "msg"); + }), r#"assertion failed: `A(1)` does not match `Foo::B(s) if s == "foo"`: msg"#); + } +} diff --git a/src/event_loop/mod.rs b/src/event_loop/mod.rs index fa749aab..118bb082 100644 --- a/src/event_loop/mod.rs +++ b/src/event_loop/mod.rs @@ -153,6 +153,7 @@ mod test { use crate::imservice::{ ContentHint, ContentPurpose }; use crate::main::PanelCommand; use crate::state::{ Application, InputMethod, InputMethodDetails, Presence, visibility }; + use crate::state::test::application_with_fake_output; fn imdetails_new() -> InputMethodDetails { InputMethodDetails { @@ -170,11 +171,12 @@ mod test { im: InputMethod::Active(imdetails_new()), physical_keyboard: Presence::Missing, visibility_override: visibility::State::NotForced, + ..application_with_fake_output(start) }; let l = State::new(state, now); let (l, commands) = handle_event(l, InputMethod::InactiveSince(now).into(), now); - assert_eq!(commands.panel_visibility, Some(PanelCommand::Show)); + assert_matches!(commands.panel_visibility, Some(PanelCommand::Show{..})); assert_eq!(l.scheduled_wakeup, Some(now + animation::HIDING_TIMEOUT)); now += animation::HIDING_TIMEOUT; diff --git a/src/lib.rs b/src/lib.rs index da991b4e..9fe36843 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,9 @@ extern crate maplit; extern crate serde; extern crate xkbcommon; +#[cfg(test)] +#[macro_use] +mod assert_matches; #[macro_use] mod logging; @@ -37,6 +40,5 @@ mod style; mod submission; pub mod tests; pub mod util; -mod ui_manager; mod vkeyboard; mod xdg; diff --git a/src/main.rs b/src/main.rs index 371cf54d..cbd1a114 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ */ /*! Glue for the main loop. */ - +use crate::outputs::OutputId; use crate::state; use glib::{Continue, MainContext, PRIORITY_DEFAULT, Receiver}; @@ -19,6 +19,7 @@ mod c { use crate::imservice::IMService; use crate::imservice::c::InputMethod; use crate::outputs::Outputs; + use crate::outputs::c::WlOutput; use crate::state; use crate::submission::Submission; use crate::util::c::Wrapped; @@ -74,7 +75,7 @@ mod c { extern "C" { #[allow(improper_ctypes)] fn init_wayland(wayland: *mut Wayland); - fn server_context_service_real_show_keyboard(service: *const UIManager); + fn server_context_service_update_keyboard(service: *const UIManager, output: WlOutput, height: u32); fn server_context_service_real_hide_keyboard(service: *const UIManager); fn server_context_service_set_hint_purpose(service: *const UIManager, hint: u32, purpose: u32); // This should probably only get called from the gtk main loop, @@ -149,8 +150,8 @@ mod c { dbus_handler: *const DBusHandler, ) { match msg.panel_visibility { - Some(PanelCommand::Show) => unsafe { - server_context_service_real_show_keyboard(ui_manager); + Some(PanelCommand::Show { output, height }) => unsafe { + server_context_service_update_keyboard(ui_manager, output.0, height); }, Some(PanelCommand::Hide) => unsafe { server_context_service_real_hide_keyboard(ui_manager); @@ -178,7 +179,10 @@ mod c { #[derive(Clone, PartialEq, Debug)] pub enum PanelCommand { - Show, + Show { + output: OutputId, + height: u32, + }, Hide, } diff --git a/src/outputs.rs b/src/outputs.rs index e82995bc..13c1b5d3 100644 --- a/src/outputs.rs +++ b/src/outputs.rs @@ -23,7 +23,7 @@ pub mod c { // Defined in C #[repr(transparent)] - #[derive(Clone, PartialEq, Copy, Debug)] + #[derive(Clone, PartialEq, Copy, Debug, Eq, Hash)] pub struct WlOutput(*const c_void); impl WlOutput { @@ -118,26 +118,6 @@ pub mod c { /// Wrapping Outputs is required for calling its methods from C type COutputs = Wrapped; - /// A stable reference to an output. - #[derive(Clone)] - #[repr(C)] - pub struct OutputHandle { - wl_output: WlOutput, - outputs: COutputs, - } - - impl OutputHandle { - // Cannot return an Output reference - // because COutputs is too deeply wrapped - pub fn get_state(&self) -> Option { - let outputs = self.outputs.clone_ref(); - let outputs = outputs.borrow(); - outputs - .find_output(self.wl_output.clone()) - .map(|o| o.current.clone()) - } - } - // Defined in Rust // Callbacks from the output listener follow @@ -303,17 +283,6 @@ pub mod c { .unwrap_or(WlOutput::null()) } - #[no_mangle] - pub extern "C" - fn squeek_outputs_get_current(raw_collection: COutputs) -> OutputHandle { - let collection = raw_collection.clone_ref(); - let collection = collection.borrow(); - OutputHandle { - wl_output: collection.outputs[0].0.output.clone(), - outputs: raw_collection.clone(), - } - } - // TODO: handle unregistration } @@ -326,15 +295,15 @@ pub struct Size { /// wl_output mode #[derive(Clone, Copy, Debug)] -struct Mode { +pub struct Mode { width: i32, height: i32, } #[derive(Clone, Copy, Debug)] pub struct OutputState { - current_mode: Option, - transform: Option, + pub current_mode: Option, + pub transform: Option, pub scale: i32, } @@ -382,8 +351,8 @@ impl OutputState { /// Not guaranteed to exist, /// but can be used to look up state. -#[derive(Clone, Copy, PartialEq, Debug)] -pub struct OutputId(c::WlOutput); +#[derive(Clone, Copy, PartialEq, Debug, Eq, Hash)] +pub struct OutputId(pub c::WlOutput); // WlOutput is a pointer, // but in the public interface, @@ -399,8 +368,10 @@ struct Output { #[derive(Debug)] struct NotFound; +/// Wayland global ID type type GlobalId = u32; +/// The outputs manager pub struct Outputs { outputs: Vec<(Output, GlobalId)>, sender: event_loop::driver::Threaded, @@ -435,14 +406,6 @@ impl Outputs { } } - fn find_output(&self, wl_output: c::WlOutput) -> Option<&Output> { - self.outputs - .iter() - .find_map(|(o, _global)| - if o.output == wl_output { Some(o) } else { None } - ) - } - fn find_output_mut(&mut self, wl_output: c::WlOutput) -> Option<&mut Output> { @@ -463,6 +426,6 @@ pub enum ChangeType { #[derive(Clone, Copy, Debug)] pub struct Event { - output: OutputId, - change: ChangeType, + pub output: OutputId, + pub change: ChangeType, } diff --git a/src/server-context-service.c b/src/server-context-service.c index 92bc3a10..aa88ed6e 100644 --- a/src/server-context-service.c +++ b/src/server-context-service.c @@ -27,6 +27,7 @@ #include "submission.h" #include "wayland.h" #include "server-context-service.h" +#include "wayland-client-protocol.h" enum { PROP_0, @@ -41,11 +42,12 @@ struct _ServerContextService { /// Needed for instantiating the widget struct submission *submission; // unowned struct squeek_layout_state *layout; - struct ui_manager *manager; // unowned struct squeek_state_manager *state_manager; // shared reference PhoshLayerSurface *window; GtkWidget *widget; // nullable + + struct wl_output *current_output; guint last_requested_height; }; @@ -64,96 +66,17 @@ on_destroy (ServerContextService *self, GtkWidget *widget) //eekboard_context_service_destroy (EEKBOARD_CONTEXT_SERVICE (context)); } -static uint32_t -calculate_height(int32_t width, GdkRectangle *geometry) -{ - uint32_t height; - if (geometry->width > geometry->height) { - // 1:5 ratio works fine on lanscape mode, and makes sure there's - // room left for the app window - height = width / 5; - } else { - if (width < 540 && width > 0) { - height = ((unsigned)width * 7 / 12); // to match 360×210 - } else { - // Here we switch to wide layout, less height needed - height = ((unsigned)width * 7 / 22); - } - } - return height; -} - static void -on_surface_configure(ServerContextService *self, PhoshLayerSurface *surface) -{ - GdkDisplay *display = NULL; - GdkWindow *window = NULL; - GdkMonitor *monitor = NULL; - GdkRectangle geometry; - gint width; - gint height; - - g_return_if_fail (SERVER_IS_CONTEXT_SERVICE (self)); - g_return_if_fail (PHOSH_IS_LAYER_SURFACE (surface)); - - g_object_get(G_OBJECT(surface), - "configured-width", &width, - "configured-height", &height, - NULL); - - // In order to improve height calculation, we need the monitor geometry so - // we can use different algorithms for portrait and landscape mode. - // Note: this is a temporary fix until the size manager is complete. - display = gdk_display_get_default (); - if (display) { - window = gtk_widget_get_window (GTK_WIDGET (surface)); - } - if (window) { - monitor = gdk_display_get_monitor_at_window (display, window); - } - if (monitor) { - gdk_monitor_get_geometry (monitor, &geometry); - } else { - geometry.width = geometry.height = 0; - } - - // When the geometry event comes after surface.configure, - // this entire height calculation does nothing. - // guint desired_height = squeek_uiman_get_perceptual_height(context->manager); - // Temporarily use old method, until the size manager is complete. - guint desired_height = calculate_height(width, &geometry); - - guint configured_height = (guint)height; - // if height was already requested once but a different one was given - // (for the same set of surrounding properties), - // then it's probably not reasonable to ask for it again, - // as it's likely to create pointless loops - // of request->reject->request_again->... - if (desired_height != configured_height - && self->last_requested_height != desired_height) { - self->last_requested_height = desired_height; - phosh_layer_surface_set_size(surface, 0, - (gint)desired_height); - phosh_layer_surface_set_exclusive_zone(surface, (gint)desired_height); - phosh_layer_surface_wl_surface_commit (surface); - } -} - -static void -make_window (ServerContextService *self) +make_window (ServerContextService *self, struct wl_output *output, uint32_t height) { if (self->window) { g_error("Window already present"); } - struct squeek_output_handle output = squeek_outputs_get_current(squeek_wayland->outputs); - squeek_uiman_set_output(self->manager, output); - uint32_t height = squeek_uiman_get_perceptual_height(self->manager); - self->window = g_object_new ( PHOSH_TYPE_LAYER_SURFACE, "layer-shell", squeek_wayland->layer_shell, - "wl-output", output.output, + "wl-output", output, "height", height, "anchor", ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM | ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT @@ -167,7 +90,7 @@ make_window (ServerContextService *self) g_object_connect (self->window, "swapped-signal::destroy", G_CALLBACK(on_destroy), self, - "swapped-signal::configured", G_CALLBACK(on_surface_configure), self, + //"swapped-signal::configured", G_CALLBACK(on_surface_configure), self, NULL); // The properties below are just to make hacking easier. @@ -204,11 +127,49 @@ make_widget (ServerContextService *self) } // Called from rust +/// Updates the type of hiddenness void -server_context_service_real_show_keyboard (ServerContextService *self) +server_context_service_real_hide_keyboard (ServerContextService *self) { + //self->desired_height = 0; + self->current_output = NULL; + if (self->window) { + gtk_widget_hide (GTK_WIDGET(self->window)); + } +} + +// Called from rust +/// Updates the type of visibility +void +server_context_service_update_keyboard (ServerContextService *self, struct wl_output *output, uint32_t height) +{ + if (output != self->current_output) { + // Recreate on a new output + server_context_service_real_hide_keyboard(self); + } else { + gint h; + PhoshLayerSurface *surface = self->window; + g_object_get(G_OBJECT(surface), + "configured-height", &h, + NULL); + + if ((uint32_t)h != height) { + + //TODO: make sure that redrawing happens in the correct place (it doesn't now). + phosh_layer_surface_set_size(self->window, 0, height); + phosh_layer_surface_set_exclusive_zone(self->window, height); + phosh_layer_surface_wl_surface_commit(self->window); + + self->current_output = output; + + return; + } + } + + self->current_output = output; + if (!self->window) { - make_window (self); + make_window (self, output, height); } if (!self->widget) { make_widget (self); @@ -216,14 +177,6 @@ server_context_service_real_show_keyboard (ServerContextService *self) gtk_widget_show (GTK_WIDGET(self->window)); } -// Called from rust -void -server_context_service_real_hide_keyboard (ServerContextService *self) -{ - if (self->window) { - gtk_widget_hide (GTK_WIDGET(self->window)); - } -} static void server_context_service_set_property (GObject *object, @@ -318,13 +271,12 @@ init (ServerContextService *self) { } ServerContextService * -server_context_service_new (EekboardContextService *self, struct submission *submission, struct squeek_layout_state *layout, struct ui_manager *uiman, struct squeek_state_manager *state_manager) +server_context_service_new (EekboardContextService *self, struct submission *submission, struct squeek_layout_state *layout, struct squeek_state_manager *state_manager) { ServerContextService *ui = g_object_new (SERVER_TYPE_CONTEXT_SERVICE, NULL); ui->submission = submission; ui->state = self; ui->layout = layout; - ui->manager = uiman; ui->state_manager = state_manager; init(ui); return ui; diff --git a/src/server-context-service.h b/src/server-context-service.h index 588aeb5d..599da818 100644 --- a/src/server-context-service.h +++ b/src/server-context-service.h @@ -20,7 +20,6 @@ #include "src/layout.h" #include "src/submission.h" -#include "ui_manager.h" G_BEGIN_DECLS @@ -29,7 +28,7 @@ G_BEGIN_DECLS /** Manages the lifecycle of the window displaying layouts. */ G_DECLARE_FINAL_TYPE (ServerContextService, server_context_service, SERVER, CONTEXT_SERVICE, GObject) -ServerContextService *server_context_service_new(EekboardContextService *self, struct submission *submission, struct squeek_layout_state *layout, struct ui_manager *uiman, struct squeek_state_manager *state_manager); +ServerContextService *server_context_service_new(EekboardContextService *self, struct submission *submission, struct squeek_layout_state *layout, struct squeek_state_manager *state_manager); enum squeek_arrangement_kind server_context_service_get_layout_type(ServerContextService *); G_END_DECLS #endif /* SERVER_CONTEXT_SERVICE_H */ diff --git a/src/server-main.c b/src/server-main.c index ec72aecc..57381ccf 100644 --- a/src/server-main.c +++ b/src/server-main.c @@ -33,7 +33,6 @@ #include "outputs.h" #include "submission.h" #include "server-context-service.h" -#include "ui_manager.h" #include "wayland.h" #include @@ -56,8 +55,6 @@ struct squeekboard { ServerContextService *ui_context; // mess, includes the entire UI /// Currently wanted layout. TODO: merge into state::Application struct squeek_layout_state layout_choice; - /// UI shape tracker/chooser. TODO: merge into state::Application - struct ui_manager *ui_manager; }; @@ -400,8 +397,6 @@ main (int argc, char **argv) // Also initializes wayland struct rsobjects rsobjects = squeek_init(); - instance.ui_manager = squeek_uiman_new(); - instance.settings_context = eekboard_context_service_new(&instance.layout_choice); // set up dbus @@ -446,7 +441,6 @@ main (int argc, char **argv) instance.settings_context, rsobjects.submission, &instance.layout_choice, - instance.ui_manager, rsobjects.state_manager); if (!ui_context) { g_error("Could not initialize GUI"); diff --git a/src/state.rs b/src/state.rs index 9009f1f4..c65033df 100644 --- a/src/state.rs +++ b/src/state.rs @@ -9,9 +9,11 @@ use crate::animation; use crate::imservice::{ ContentHint, ContentPurpose }; use crate::main::{ Commands, PanelCommand }; use crate::outputs; +use crate::outputs::{OutputId, OutputState}; +use std::cmp; +use std::collections::HashMap; use std::time::Instant; - #[derive(Clone, Copy)] pub enum Presence { Present, @@ -92,12 +94,12 @@ impl Outcome { pub fn get_commands_to_reach(&self, new_state: &Self) -> Commands { let layout_hint_set = match new_state { Outcome { - visibility: animation::Outcome::Visible, + visibility: animation::Outcome::Visible{..}, im: InputMethod::Active(hints), } => Some(hints.clone()), Outcome { - visibility: animation::Outcome::Visible, + visibility: animation::Outcome::Visible{..}, im: InputMethod::InactiveSince(_), } => Some(InputMethodDetails { hint: ContentHint::NONE, @@ -109,9 +111,10 @@ impl Outcome { .. } => None, }; - +// FIXME: handle switching outputs let (dbus_visible_set, panel_visibility) = match new_state.visibility { - animation::Outcome::Visible => (Some(true), Some(PanelCommand::Show)), + animation::Outcome::Visible{output, height} + => (Some(true), Some(PanelCommand::Show{output, height})), animation::Outcome::Hidden => (Some(false), Some(PanelCommand::Hide)), }; @@ -139,6 +142,13 @@ pub struct Application { pub im: InputMethod, pub visibility_override: visibility::State, pub physical_keyboard: Presence, + /// The output on which the panel should appear. + /// This is stored as part of the state + /// because it's not clear how to derive the output from the rest of the state. + /// It should probably follow the focused input, + /// but not sure about being allowed on non-touch displays. + pub preferred_output: Option, + pub outputs: HashMap, } impl Application { @@ -153,6 +163,8 @@ impl Application { im: InputMethod::InactiveSince(now), visibility_override: visibility::State::NotForced, physical_keyboard: Presence::Missing, + preferred_output: None, + outputs: Default::default(), } } @@ -173,9 +185,23 @@ impl Application { ..self }, - Event::Output(output) => { - println!("Stub: output event {:?}", output); - self + Event::Output(outputs::Event { output, change }) => { + let mut app = self; + match change { + outputs::ChangeType::Altered(state) => { + app.outputs.insert(output, state); + app.preferred_output = app.preferred_output.or(Some(output)); + }, + outputs::ChangeType::Removed => { + app.outputs.remove(&output); + if app.preferred_output == Some(output) { + // There's currently no policy to choose one output over another, + // so just take whichever comes first. + app.preferred_output = app.outputs.keys().next().map(|output| *output); + } + }, + }; + app }, Event::InputMethod(new_im) => match (self.im.clone(), new_im) { @@ -212,20 +238,51 @@ impl Application { } } + fn get_preferred_height(output: &OutputState) -> Option { + output.get_pixel_size() + .map(|px_size| { + let height = { + if px_size.width > px_size.height { + px_size.width / 5 + } else { + if (px_size.width < 540) & (px_size.width > 0) { + px_size.width * 7 / 12 // to match 360×210 + } else { + // Here we switch to wide layout, less height needed + px_size.width * 7 / 22 + } + } + }; + cmp::min(height, px_size.height / 2) + }) + } + pub fn get_outcome(&self, now: Instant) -> Outcome { // FIXME: include physical keyboard presence Outcome { - visibility: match (self.physical_keyboard, self.visibility_override) { - (_, visibility::State::ForcedHidden) => animation::Outcome::Hidden, - (_, visibility::State::ForcedVisible) => animation::Outcome::Visible, - (Presence::Present, visibility::State::NotForced) => animation::Outcome::Hidden, - (Presence::Missing, visibility::State::NotForced) => match self.im { - InputMethod::Active(_) => animation::Outcome::Visible, - InputMethod::InactiveSince(since) => { - if now < since + animation::HIDING_TIMEOUT { animation::Outcome::Visible } - else { animation::Outcome::Hidden } - }, - }, + visibility: match self.preferred_output { + None => animation::Outcome::Hidden, + Some(output) => { + // Hoping that this will get optimized out on branches not using `visible`. + let height = Self::get_preferred_height(self.outputs.get(&output).unwrap()) + .unwrap_or(0); + // TODO: Instead of setting size to 0 when the output is invalid, + // simply go invisible. + let visible = animation::Outcome::Visible{output, height}; + + match (self.physical_keyboard, self.visibility_override) { + (_, visibility::State::ForcedHidden) => animation::Outcome::Hidden, + (_, visibility::State::ForcedVisible) => visible, + (Presence::Present, visibility::State::NotForced) => animation::Outcome::Hidden, + (Presence::Missing, visibility::State::NotForced) => match self.im { + InputMethod::Active(_) => visible, + InputMethod::InactiveSince(since) => { + if now < since + animation::HIDING_TIMEOUT { visible } + else { animation::Outcome::Hidden } + }, + }, + } + } }, im: self.im.clone(), } @@ -250,9 +307,9 @@ impl Application { #[cfg(test)] -mod test { +pub mod test { use super::*; - + use crate::outputs::c::WlOutput; use std::time::Duration; fn imdetails_new() -> InputMethodDetails { @@ -262,6 +319,30 @@ mod test { } } + fn fake_output_id(id: usize) -> OutputId { + OutputId(unsafe { + std::mem::transmute::<_, WlOutput>(id) + }) + } + + pub fn application_with_fake_output(start: Instant) -> Application { + let id = fake_output_id(1); + let mut outputs = HashMap::new(); + outputs.insert( + id, + OutputState { + current_mode: None, + transform: None, + scale: 1, + }, + ); + Application { + preferred_output: Some(id), + outputs, + ..Application::new(start) + } + } + /// Test the original delay scenario: no flicker on quick switches. #[test] fn avoid_hide() { @@ -271,15 +352,16 @@ mod test { im: InputMethod::Active(imdetails_new()), physical_keyboard: Presence::Missing, visibility_override: visibility::State::NotForced, + ..application_with_fake_output(start) }; let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now); // Check 100ms at 1ms intervals. It should remain visible. for _i in 0..100 { now += Duration::from_millis(1); - assert_eq!( + assert_matches!( state.get_outcome(now).visibility, - animation::Outcome::Visible, + animation::Outcome::Visible{..}, "Hidden when it should remain visible: {:?}", now.saturating_duration_since(start), ) @@ -287,7 +369,7 @@ mod test { let state = state.apply_event(Event::InputMethod(InputMethod::Active(imdetails_new())), now); - assert_eq!(state.get_outcome(now).visibility, animation::Outcome::Visible); + assert_matches!(state.get_outcome(now).visibility, animation::Outcome::Visible{..}); } /// Make sure that hiding works when input method goes away @@ -299,11 +381,12 @@ mod test { im: InputMethod::Active(imdetails_new()), physical_keyboard: Presence::Missing, visibility_override: visibility::State::NotForced, + ..application_with_fake_output(start) }; let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now); - while let animation::Outcome::Visible = state.get_outcome(now).visibility { + while let animation::Outcome::Visible{..} = state.get_outcome(now).visibility { now += Duration::from_millis(1); assert!( now < start + Duration::from_millis(250), @@ -323,6 +406,7 @@ mod test { im: InputMethod::Active(imdetails_new()), physical_keyboard: Presence::Missing, visibility_override: visibility::State::NotForced, + ..application_with_fake_output(start) }; // This reflects the sequence from Wayland: // disable, disable, enable, disable @@ -332,7 +416,7 @@ mod test { let state = state.apply_event(Event::InputMethod(InputMethod::Active(imdetails_new())), now); let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now); - while let animation::Outcome::Visible = state.get_outcome(now).visibility { + while let animation::Outcome::Visible{..} = state.get_outcome(now).visibility { now += Duration::from_millis(1); assert!( now < start + Duration::from_millis(250), @@ -361,13 +445,14 @@ mod test { im: InputMethod::InactiveSince(now), physical_keyboard: Presence::Missing, visibility_override: visibility::State::NotForced, + ..application_with_fake_output(start) }; now += Duration::from_secs(1); let state = state.apply_event(Event::Visibility(visibility::Event::ForceVisible), now); - assert_eq!( + assert_matches!( state.get_outcome(now).visibility, - animation::Outcome::Visible, + animation::Outcome::Visible{..}, "Failed to show: {:?}", now.saturating_duration_since(start), ); @@ -394,6 +479,7 @@ mod test { im: InputMethod::Active(imdetails_new()), physical_keyboard: Presence::Missing, visibility_override: visibility::State::NotForced, + ..application_with_fake_output(start) }; now += Duration::from_secs(1); @@ -420,9 +506,9 @@ mod test { now += Duration::from_secs(1); let state = state.apply_event(Event::PhysicalKeyboard(Presence::Missing), now); - assert_eq!( + assert_matches!( state.get_outcome(now).visibility, - animation::Outcome::Visible, + animation::Outcome::Visible{..}, "Failed to appear: {:?}", now.saturating_duration_since(start), ); diff --git a/src/submission.h b/src/submission.h index ed1093f8..5ee2f727 100644 --- a/src/submission.h +++ b/src/submission.h @@ -5,7 +5,6 @@ #include "virtual-keyboard-unstable-v1-client-protocol.h" #include "eek/eek-types.h" #include "main.h" -#include "src/ui_manager.h" struct squeek_layout; diff --git a/src/ui_manager.h b/src/ui_manager.h deleted file mode 100644 index b5f1734e..00000000 --- a/src/ui_manager.h +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef UI_MANAGER__ -#define UI_MANAGER__ - -#include - -#include "eek/eek-types.h" -#include "outputs.h" -#include "main.h" - -struct ui_manager; - -struct ui_manager *squeek_uiman_new(void); -void squeek_uiman_set_output(struct ui_manager *uiman, struct squeek_output_handle output); -uint32_t squeek_uiman_get_perceptual_height(struct ui_manager *uiman); - -struct vis_manager; - -struct vis_manager *squeek_visman_new(struct squeek_state_manager *state_manager); -#endif diff --git a/src/ui_manager.rs b/src/ui_manager.rs deleted file mode 100644 index b189a479..00000000 --- a/src/ui_manager.rs +++ /dev/null @@ -1,82 +0,0 @@ -/* Copyright (C) 2020, 2021 Purism SPC - * SPDX-License-Identifier: GPL-3.0+ - */ - -/*! Centrally manages the shape of the UI widgets, and the choice of layout. - * - * Coordinates this based on information collated from all possible sources. - */ - -use std::cmp::min; -use ::outputs::c::OutputHandle; - - -pub mod c { - use super::*; - use ::util::c::Wrapped; - - #[no_mangle] - pub extern "C" - fn squeek_uiman_new() -> Wrapped { - Wrapped::new(Manager { output: None }) - } - - /// Used to size the layer surface containing all the OSK widgets. - #[no_mangle] - pub extern "C" - fn squeek_uiman_get_perceptual_height( - uiman: Wrapped, - ) -> u32 { - let uiman = uiman.clone_ref(); - let uiman = uiman.borrow(); - // TODO: what to do when there's no output? - uiman.get_perceptual_height().unwrap_or(0) - } - - #[no_mangle] - pub extern "C" - fn squeek_uiman_set_output( - uiman: Wrapped, - output: OutputHandle, - ) { - let uiman = uiman.clone_ref(); - let mut uiman = uiman.borrow_mut(); - uiman.output = Some(output); - } -} - -/// Stores current state of all things influencing what the UI should look like. -pub struct Manager { - /// Shared output handle, current state updated whenever it's needed. - // TODO: Stop assuming that the output never changes. - // (There's no way for the output manager to update the ui manager.) - // FIXME: Turn into an OutputState and apply relevant connections elsewhere. - // Otherwise testability and predictablity is low. - output: Option, - //// Pixel size of the surface. Needs explicit updating. - //surface_size: Option, -} - -impl Manager { - fn get_perceptual_height(&self) -> Option { - let output_info = (&self.output).as_ref() - .and_then(|o| o.get_state()) - .map(|os| (os.scale as u32, os.get_pixel_size())); - match output_info { - Some((scale, Some(px_size))) => Some({ - let height = if (px_size.width < 720) & (px_size.width > 0) { - px_size.width * 7 / 12 // to match 360×210 - } else if px_size.width < 1080 { - 360 + (1080 - px_size.width) * 60 / 360 // smooth transition - } else { - 360 - }; - - // Don't exceed half the display size - min(height, px_size.height / 2) / scale - }), - Some((scale, None)) => Some(360 / scale), - None => None, - } - } -}