event_loop: Separate and use for physical keyboard presence
Extra included: Change of naked Submission pointers to Wrapped.
This commit is contained in:
		@ -1,13 +0,0 @@
 | 
				
			|||||||
#pragma once
 | 
					 | 
				
			||||||
#include <gtk/gtk.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// from main.h
 | 
					 | 
				
			||||||
struct sender;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// from animations.rs
 | 
					 | 
				
			||||||
struct squeek_animation_visibility_manager;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct squeek_animation_visibility_manager *squeek_animation_visibility_manager_new(struct sender *ui_sender);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
void squeek_animation_visibility_manager_send_claim_visible(struct squeek_animation_visibility_manager *animman);
 | 
					 | 
				
			||||||
void squeek_animation_visibility_manager_send_force_hide(struct squeek_animation_visibility_manager *animman);
 | 
					 | 
				
			||||||
							
								
								
									
										433
									
								
								src/animation.rs
									
									
									
									
									
								
							
							
						
						
									
										433
									
								
								src/animation.rs
									
									
									
									
									
								
							@ -2,441 +2,16 @@
 | 
				
			|||||||
 * SPDX-License-Identifier: GPL-3.0+
 | 
					 * SPDX-License-Identifier: GPL-3.0+
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/*! Animation state trackers and drivers.
 | 
					/*! Animation details */
 | 
				
			||||||
 * Concerns the presentation layer.
 | 
					 | 
				
			||||||
 * 
 | 
					 | 
				
			||||||
 * Documentation and comments in this module
 | 
					 | 
				
			||||||
 * are meant to be read from the top to bottom. */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use crate::logging;
 | 
					 | 
				
			||||||
use glib;
 | 
					 | 
				
			||||||
use std::cmp;
 | 
					 | 
				
			||||||
use std::sync::mpsc;
 | 
					 | 
				
			||||||
use std::time::{ Duration, Instant };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Traits
 | 
					 | 
				
			||||||
use crate::logging::Warn;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::time::Duration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// The keyboard should hide after this has elapsed to prevent flickering.
 | 
					/// The keyboard should hide after this has elapsed to prevent flickering.
 | 
				
			||||||
const HIDING_TIMEOUT: Duration = Duration::from_millis(200);
 | 
					pub const HIDING_TIMEOUT: Duration = Duration::from_millis(200);
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Events that the state tracker processes
 | 
					 | 
				
			||||||
#[derive(Clone)]
 | 
					 | 
				
			||||||
pub enum Event {
 | 
					 | 
				
			||||||
    ClaimVisible,
 | 
					 | 
				
			||||||
    /// The panel is not needed
 | 
					 | 
				
			||||||
    ReleaseVisible,
 | 
					 | 
				
			||||||
    /// The user requested the panel to go down
 | 
					 | 
				
			||||||
    ForceHide,
 | 
					 | 
				
			||||||
    /// Event triggered because a moment in time passed.
 | 
					 | 
				
			||||||
    /// Use to animate state transitions.
 | 
					 | 
				
			||||||
    /// The value is the ideal arrival time.
 | 
					 | 
				
			||||||
    TimeoutReached(Instant),
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// The outwardly visible state of visibility
 | 
					/// The outwardly visible state of visibility
 | 
				
			||||||
#[derive(PartialEq, Debug)]
 | 
					#[derive(PartialEq, Debug, Clone)]
 | 
				
			||||||
pub enum Outcome {
 | 
					pub enum Outcome {
 | 
				
			||||||
    Visible,
 | 
					    Visible,
 | 
				
			||||||
    Hidden,
 | 
					    Hidden,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// The actual logic of visibility animation.
 | 
					 | 
				
			||||||
/// It keeps the pael visible for a short time period after each hide request.
 | 
					 | 
				
			||||||
/// This prevents flickering on quick successive enable/disable events.
 | 
					 | 
				
			||||||
/// It does not treat user-driven hiding in a special way.
 | 
					 | 
				
			||||||
///
 | 
					 | 
				
			||||||
/// This is the "functional core".
 | 
					 | 
				
			||||||
/// All state changes return the next state and the optimal time for the next check.
 | 
					 | 
				
			||||||
///
 | 
					 | 
				
			||||||
/// This state tracker can be driven by any event loop.
 | 
					 | 
				
			||||||
#[derive(Clone, PartialEq, Debug)]
 | 
					 | 
				
			||||||
enum VisibilityTracker {
 | 
					 | 
				
			||||||
    Visible,
 | 
					 | 
				
			||||||
    /// Wait until the instant is reached and then hide immediately.
 | 
					 | 
				
			||||||
    /// Depending on the relation to current time, it means either visible or hidden.
 | 
					 | 
				
			||||||
    HiddenAfter(Instant),
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use self::VisibilityTracker::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl VisibilityTracker {
 | 
					 | 
				
			||||||
    fn apply_event(self, event: Event, now: Instant) -> Self {
 | 
					 | 
				
			||||||
        match event {
 | 
					 | 
				
			||||||
            Event::ClaimVisible => Visible,
 | 
					 | 
				
			||||||
            Event::ReleaseVisible => match self {
 | 
					 | 
				
			||||||
                Visible => HiddenAfter(now + HIDING_TIMEOUT),
 | 
					 | 
				
			||||||
                other => other,
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            Event::ForceHide => match self {
 | 
					 | 
				
			||||||
                // Special case to avoid unneeded state changes.
 | 
					 | 
				
			||||||
                HiddenAfter(when) => HiddenAfter(cmp::min(when, now)),
 | 
					 | 
				
			||||||
                _ => HiddenAfter(now),
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            // The tracker doesn't change just because time is passing.
 | 
					 | 
				
			||||||
            Event::TimeoutReached(_) => self,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Returns the state visible to the outside
 | 
					 | 
				
			||||||
    fn get_outcome(&self, now: Instant) -> Outcome {
 | 
					 | 
				
			||||||
        let visible = match self {
 | 
					 | 
				
			||||||
            Visible => true,
 | 
					 | 
				
			||||||
            HiddenAfter(hide_after) => *hide_after > now,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
        if visible {
 | 
					 | 
				
			||||||
            Outcome::Visible
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            Outcome::Hidden
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Returns the next time to update the state.
 | 
					 | 
				
			||||||
    fn get_next_wake(&self, now: Instant) -> Option<Instant> {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            HiddenAfter(next) => {
 | 
					 | 
				
			||||||
                if *next > now { Some(*next) }
 | 
					 | 
				
			||||||
                else { None }
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            _ => None,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* If we performed updates in a tight loop,
 | 
					 | 
				
			||||||
 * the Tracker would have been all we need.
 | 
					 | 
				
			||||||
 * 
 | 
					 | 
				
			||||||
 * loop {
 | 
					 | 
				
			||||||
 *     event = current_event()
 | 
					 | 
				
			||||||
 *     outcome = update_state(event)
 | 
					 | 
				
			||||||
 *     window.apply(outcome)
 | 
					 | 
				
			||||||
 * }
 | 
					 | 
				
			||||||
 * 
 | 
					 | 
				
			||||||
 * This is enough to process all events,
 | 
					 | 
				
			||||||
 * and keep the window always in sync with the current state.
 | 
					 | 
				
			||||||
 * 
 | 
					 | 
				
			||||||
 * However, we're trying to be conservative,
 | 
					 | 
				
			||||||
 * and not waste time performing updates that don't change state,
 | 
					 | 
				
			||||||
 * so we have to react to events that end up influencing the state.
 | 
					 | 
				
			||||||
 * 
 | 
					 | 
				
			||||||
 * One complication from that is that animation steps
 | 
					 | 
				
			||||||
 * are not a response to events coming from the owner of the loop,
 | 
					 | 
				
			||||||
 * but are needed by the loop itself.
 | 
					 | 
				
			||||||
 *
 | 
					 | 
				
			||||||
 * This is where the rest of bugs hide:
 | 
					 | 
				
			||||||
 * too few scheduled wakeups mean missed updates and wrong visible state.
 | 
					 | 
				
			||||||
 * Too many wakeups can slow down the process, or make animation jittery.
 | 
					 | 
				
			||||||
 * The loop iteration is kept as a pure function to stay testable.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// This keeps the state of the tracker loop between iterations
 | 
					 | 
				
			||||||
#[derive(Clone)]
 | 
					 | 
				
			||||||
struct LoopState {
 | 
					 | 
				
			||||||
    state: VisibilityTracker,
 | 
					 | 
				
			||||||
    scheduled_wakeup: Option<Instant>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl LoopState {
 | 
					 | 
				
			||||||
    fn new(initial_state: VisibilityTracker) -> Self {
 | 
					 | 
				
			||||||
        Self {
 | 
					 | 
				
			||||||
            state: initial_state,
 | 
					 | 
				
			||||||
            scheduled_wakeup: None,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// A single iteration of the loop, updating its persistent state.
 | 
					 | 
				
			||||||
/// - updates tracker state,
 | 
					 | 
				
			||||||
/// - determines outcome,
 | 
					 | 
				
			||||||
/// - determines next scheduled animation wakeup,
 | 
					 | 
				
			||||||
/// and because this is a pure function, it's easily testable.
 | 
					 | 
				
			||||||
/// It returns the new state, and the optional message to send onwards.
 | 
					 | 
				
			||||||
fn handle_loop_event(
 | 
					 | 
				
			||||||
    mut loop_state: LoopState,
 | 
					 | 
				
			||||||
    event: Event,
 | 
					 | 
				
			||||||
    now: Instant,
 | 
					 | 
				
			||||||
) -> (LoopState, Option<Outcome>) {
 | 
					 | 
				
			||||||
    // Forward current public state to the consumer.
 | 
					 | 
				
			||||||
    // This doesn't take changes into account,
 | 
					 | 
				
			||||||
    // but we're only sending updates as a response to events,
 | 
					 | 
				
			||||||
    // so no-ops shouldn't dominate.
 | 
					 | 
				
			||||||
    loop_state.state = loop_state.state.apply_event(event.clone(), now);
 | 
					 | 
				
			||||||
    let outcome = loop_state.state.get_outcome(now);
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    // Timeout events are special: they affect the scheduled timeout.
 | 
					 | 
				
			||||||
    loop_state.scheduled_wakeup = match event {
 | 
					 | 
				
			||||||
        Event::TimeoutReached(when) => {
 | 
					 | 
				
			||||||
            if when > now {
 | 
					 | 
				
			||||||
                // Special handling for scheduled events coming in early.
 | 
					 | 
				
			||||||
                // Wait at least 10 ms to avoid Zeno's paradox.
 | 
					 | 
				
			||||||
                // This is probably not needed though,
 | 
					 | 
				
			||||||
                // if the `now` contains the desired time of the event.
 | 
					 | 
				
			||||||
                // But then what about time "reversing"?
 | 
					 | 
				
			||||||
                Some(cmp::max(
 | 
					 | 
				
			||||||
                    when,
 | 
					 | 
				
			||||||
                    now + Duration::from_millis(10),
 | 
					 | 
				
			||||||
                ))
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                // There's only one timeout in flight, and it's this one.
 | 
					 | 
				
			||||||
                // It's about to complete, and then the tracker can be cleared.
 | 
					 | 
				
			||||||
                // I'm not sure if this is strictly necessary.
 | 
					 | 
				
			||||||
                None
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        _ => loop_state.scheduled_wakeup.clone(),
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    // Reschedule timeout if the new state calls for it.
 | 
					 | 
				
			||||||
    let scheduled = &loop_state.scheduled_wakeup;
 | 
					 | 
				
			||||||
    let desired = loop_state.state.get_next_wake(now);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    loop_state.scheduled_wakeup = match (scheduled, desired) {
 | 
					 | 
				
			||||||
        (&Some(scheduled), Some(next)) => {
 | 
					 | 
				
			||||||
            if scheduled > next {
 | 
					 | 
				
			||||||
                // State wants a wake to happen before the one which is already scheduled.
 | 
					 | 
				
			||||||
                // The previous state is removed in order to only ever keep one in flight.
 | 
					 | 
				
			||||||
                // That hopefully avoids pileups,
 | 
					 | 
				
			||||||
                // e.g. because the system is busy
 | 
					 | 
				
			||||||
                // and the user keeps doing something that queues more events.
 | 
					 | 
				
			||||||
                Some(next)
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                // Not changing the case when the wanted wake is *after* scheduled,
 | 
					 | 
				
			||||||
                // because wakes are not expensive as long as they don't pile up,
 | 
					 | 
				
			||||||
                // and I can't see a pileup potential when it doesn't retrigger itself.
 | 
					 | 
				
			||||||
                // Skipping an expected event is much more dangerous.
 | 
					 | 
				
			||||||
                Some(scheduled)
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        (None, Some(next)) => Some(next),
 | 
					 | 
				
			||||||
        // No need to change the unneeded wake - see above.
 | 
					 | 
				
			||||||
        // (Some(_), None) => ...
 | 
					 | 
				
			||||||
        (other, _) => other.clone(),
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    (loop_state, Some(outcome))
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/*
 | 
					 | 
				
			||||||
 * The tracker loop needs to be driven somehow,
 | 
					 | 
				
			||||||
 * and connected to the external world,
 | 
					 | 
				
			||||||
 * both on the side of receiving and sending events.
 | 
					 | 
				
			||||||
 * 
 | 
					 | 
				
			||||||
 * That's going to be implementation-dependent,
 | 
					 | 
				
			||||||
 * connecting to some external mechanisms
 | 
					 | 
				
			||||||
 * for time, messages, and threading/callbacks.
 | 
					 | 
				
			||||||
 * 
 | 
					 | 
				
			||||||
 * This is the "imperative shell" part of the software,
 | 
					 | 
				
			||||||
 * and no longer unit-testable.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
use std::thread;
 | 
					 | 
				
			||||||
type Sender = mpsc::Sender<Event>;
 | 
					 | 
				
			||||||
type UISender = glib::Sender<Outcome>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// This loop driver spawns a new thread which updates the state in a loop,
 | 
					 | 
				
			||||||
/// in response to incoming events.
 | 
					 | 
				
			||||||
/// It sends outcomes to the glib main loop using a channel.
 | 
					 | 
				
			||||||
/// The outcomes are applied by the UI end of the channel.
 | 
					 | 
				
			||||||
// This could still be reasonably tested,
 | 
					 | 
				
			||||||
// by creating a glib::Sender and checking what messages it receives.
 | 
					 | 
				
			||||||
#[derive(Clone)]
 | 
					 | 
				
			||||||
pub struct ThreadLoopDriver {
 | 
					 | 
				
			||||||
    thread: Sender,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl ThreadLoopDriver {
 | 
					 | 
				
			||||||
    pub fn new(ui: UISender) -> Self {
 | 
					 | 
				
			||||||
        let (sender, receiver) = mpsc::channel();
 | 
					 | 
				
			||||||
        let saved_sender = sender.clone();
 | 
					 | 
				
			||||||
        thread::spawn(move || {
 | 
					 | 
				
			||||||
            let mut state = LoopState::new(VisibilityTracker::Visible);
 | 
					 | 
				
			||||||
            loop {
 | 
					 | 
				
			||||||
                match receiver.recv() {
 | 
					 | 
				
			||||||
                    Ok(event) => {
 | 
					 | 
				
			||||||
                        state = Self::handle_loop_event(&sender, state, event, &ui);
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                    Err(e) => {
 | 
					 | 
				
			||||||
                        logging::print(logging::Level::Bug, &format!("Senders hung up, aborting: {}", e));
 | 
					 | 
				
			||||||
                        return;
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                };
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        Self {
 | 
					 | 
				
			||||||
            thread: saved_sender,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    pub fn send(&self, event: Event) -> Result<(), mpsc::SendError<Event>> {
 | 
					 | 
				
			||||||
        self.thread.send(event)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    fn handle_loop_event(loop_sender: &Sender, state: LoopState, event: Event, ui: &UISender) -> LoopState {
 | 
					 | 
				
			||||||
        let now = Instant::now();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let (new_state, outcome) = handle_loop_event(state.clone(), event, now);
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        if let Some(outcome) = outcome {
 | 
					 | 
				
			||||||
            ui.send(outcome)
 | 
					 | 
				
			||||||
                .or_warn(&mut   logging::Print, logging::Problem::Bug, "Can't send to UI");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if new_state.scheduled_wakeup != state.scheduled_wakeup {
 | 
					 | 
				
			||||||
            if let Some(when) = new_state.scheduled_wakeup {
 | 
					 | 
				
			||||||
                Self::schedule_timeout_wake(loop_sender, when);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        new_state
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    fn schedule_timeout_wake(loop_sender: &Sender, when: Instant) {
 | 
					 | 
				
			||||||
        let sender = loop_sender.clone();
 | 
					 | 
				
			||||||
        thread::spawn(move || {
 | 
					 | 
				
			||||||
            let now = Instant::now();
 | 
					 | 
				
			||||||
            thread::sleep(when - now);
 | 
					 | 
				
			||||||
            sender.send(Event::TimeoutReached(when))
 | 
					 | 
				
			||||||
                .or_warn(&mut logging::Print, logging::Problem::Warning, "Can't wake visibility manager");
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// For calling in only
 | 
					 | 
				
			||||||
mod c {
 | 
					 | 
				
			||||||
    use super::*;
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    use util::c::{ ArcWrapped, Wrapped };
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    #[no_mangle]
 | 
					 | 
				
			||||||
    pub extern "C"
 | 
					 | 
				
			||||||
    fn squeek_animation_visibility_manager_new(sender: ArcWrapped<UISender>)
 | 
					 | 
				
			||||||
        -> Wrapped<ThreadLoopDriver>
 | 
					 | 
				
			||||||
    {
 | 
					 | 
				
			||||||
        let sender = sender.clone_ref();
 | 
					 | 
				
			||||||
        let sender = sender.lock().unwrap();
 | 
					 | 
				
			||||||
        Wrapped::new(ThreadLoopDriver::new(sender.clone()))
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    #[no_mangle]
 | 
					 | 
				
			||||||
    pub extern "C"
 | 
					 | 
				
			||||||
    fn squeek_animation_visibility_manager_send_claim_visible(mgr: Wrapped<ThreadLoopDriver>) {
 | 
					 | 
				
			||||||
        let sender = mgr.clone_ref();
 | 
					 | 
				
			||||||
        let sender = sender.borrow();
 | 
					 | 
				
			||||||
        sender.send(Event::ClaimVisible)
 | 
					 | 
				
			||||||
            .or_warn(&mut logging::Print, logging::Problem::Warning, "Can't send to visibility manager");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    #[no_mangle]
 | 
					 | 
				
			||||||
    pub extern "C"
 | 
					 | 
				
			||||||
    fn squeek_animation_visibility_manager_send_force_hide(sender: Wrapped<ThreadLoopDriver>) {
 | 
					 | 
				
			||||||
        let sender = sender.clone_ref();
 | 
					 | 
				
			||||||
        let sender = sender.borrow();
 | 
					 | 
				
			||||||
        sender.send(Event::ForceHide)
 | 
					 | 
				
			||||||
            .or_warn(&mut logging::Print, logging::Problem::Warning, "Can't send to visibility manager");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[cfg(test)]
 | 
					 | 
				
			||||||
mod test {
 | 
					 | 
				
			||||||
    use super::*;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Test the original delay scenario: no flicker on quick switches.
 | 
					 | 
				
			||||||
    #[test]
 | 
					 | 
				
			||||||
    fn hide_show() {
 | 
					 | 
				
			||||||
        let start = Instant::now(); // doesn't matter when. It would be better to have a reproducible value though
 | 
					 | 
				
			||||||
        let mut now = start;
 | 
					 | 
				
			||||||
        let state = VisibilityTracker::Visible;
 | 
					 | 
				
			||||||
        let state = state.apply_event(Event::ReleaseVisible, now);
 | 
					 | 
				
			||||||
        // Check 100ms at 1ms intervals. It should remain visible.
 | 
					 | 
				
			||||||
        for _i in 0..100 {
 | 
					 | 
				
			||||||
            now += Duration::from_millis(1);
 | 
					 | 
				
			||||||
            assert_eq!(
 | 
					 | 
				
			||||||
                state.get_outcome(now),
 | 
					 | 
				
			||||||
                Outcome::Visible,
 | 
					 | 
				
			||||||
                "Hidden when it should remain visible: {:?}",
 | 
					 | 
				
			||||||
                now.saturating_duration_since(start),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        let state = state.apply_event(Event::ClaimVisible, now);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        assert_eq!(state.get_outcome(now), Outcome::Visible);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// Make sure that hiding works when requested legitimately
 | 
					 | 
				
			||||||
    #[test]
 | 
					 | 
				
			||||||
    fn hide() {
 | 
					 | 
				
			||||||
        let start = Instant::now(); // doesn't matter when. It would be better to have a reproducible value though
 | 
					 | 
				
			||||||
        let mut now = start;
 | 
					 | 
				
			||||||
        let state = VisibilityTracker::Visible;
 | 
					 | 
				
			||||||
        let state = state.apply_event(Event::ReleaseVisible, now);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        while let Outcome::Visible = state.get_outcome(now) {
 | 
					 | 
				
			||||||
            now += Duration::from_millis(1);
 | 
					 | 
				
			||||||
            assert!(
 | 
					 | 
				
			||||||
                now < start + Duration::from_millis(250),
 | 
					 | 
				
			||||||
                "Hiding too slow: {:?}",
 | 
					 | 
				
			||||||
                now.saturating_duration_since(start),
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    /// Check against the false showing bug.
 | 
					 | 
				
			||||||
    /// Expectation: it will get hidden and not appear again
 | 
					 | 
				
			||||||
    #[test]
 | 
					 | 
				
			||||||
    fn false_show() {
 | 
					 | 
				
			||||||
        let start = Instant::now(); // doesn't matter when. It would be better to have a reproducible value though
 | 
					 | 
				
			||||||
        let mut now = start;
 | 
					 | 
				
			||||||
        let state = VisibilityTracker::Visible;
 | 
					 | 
				
			||||||
        // This reflects the sequence from Wayland:
 | 
					 | 
				
			||||||
        // disable, disable, enable, disable
 | 
					 | 
				
			||||||
        // all in a single batch.
 | 
					 | 
				
			||||||
        let state = state.apply_event(Event::ReleaseVisible, now);
 | 
					 | 
				
			||||||
        let state = state.apply_event(Event::ReleaseVisible, now);
 | 
					 | 
				
			||||||
        let state = state.apply_event(Event::ClaimVisible, now);
 | 
					 | 
				
			||||||
        let state = state.apply_event(Event::ReleaseVisible, now);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        while let Outcome::Visible = state.get_outcome(now) {
 | 
					 | 
				
			||||||
            now += Duration::from_millis(1);
 | 
					 | 
				
			||||||
            assert!(
 | 
					 | 
				
			||||||
                now < start + Duration::from_millis(250),
 | 
					 | 
				
			||||||
                "Still not hidden: {:?}",
 | 
					 | 
				
			||||||
                now.saturating_duration_since(start),
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        // One second without appearing again
 | 
					 | 
				
			||||||
        for _i in 0..1000 {
 | 
					 | 
				
			||||||
            now += Duration::from_millis(1);
 | 
					 | 
				
			||||||
            assert_eq!(
 | 
					 | 
				
			||||||
                state.get_outcome(now),
 | 
					 | 
				
			||||||
                Outcome::Hidden,
 | 
					 | 
				
			||||||
                "Appeared unnecessarily: {:?}",
 | 
					 | 
				
			||||||
                now.saturating_duration_since(start),
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    #[test]
 | 
					 | 
				
			||||||
    fn schedule_hide() {
 | 
					 | 
				
			||||||
        let start = Instant::now(); // doesn't matter when. It would be better to have a reproducible value though
 | 
					 | 
				
			||||||
        let mut now = start;
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        let l = LoopState::new(VisibilityTracker::Visible);
 | 
					 | 
				
			||||||
        let (l, outcome) = handle_loop_event(l, Event::ReleaseVisible, now);
 | 
					 | 
				
			||||||
        assert_eq!(outcome, Some(Outcome::Visible));
 | 
					 | 
				
			||||||
        assert_eq!(l.scheduled_wakeup, Some(now + HIDING_TIMEOUT));
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        now += HIDING_TIMEOUT;
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        let (l, outcome) = handle_loop_event(l, Event::TimeoutReached(now), now);
 | 
					 | 
				
			||||||
        assert_eq!(outcome, Some(Outcome::Hidden));
 | 
					 | 
				
			||||||
        assert_eq!(l.scheduled_wakeup, None);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										10
									
								
								src/dbus.c
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								src/dbus.c
									
									
									
									
									
								
							@ -19,7 +19,9 @@
 | 
				
			|||||||
#include "config.h"
 | 
					#include "config.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include "dbus.h"
 | 
					#include "dbus.h"
 | 
				
			||||||
 | 
					#include "main.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <inttypes.h>
 | 
				
			||||||
#include <stdio.h>
 | 
					#include <stdio.h>
 | 
				
			||||||
#include <gio/gio.h>
 | 
					#include <gio/gio.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -53,9 +55,9 @@ handle_set_visible(SmPuriOSK0 *object, GDBusMethodInvocation *invocation,
 | 
				
			|||||||
    DBusHandler *service = user_data;
 | 
					    DBusHandler *service = user_data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (arg_visible) {
 | 
					    if (arg_visible) {
 | 
				
			||||||
        squeek_animation_visibility_manager_send_claim_visible (service->animman);
 | 
					        squeek_state_send_force_visible (service->state_manager);
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        squeek_animation_visibility_manager_send_force_hide (service->animman);
 | 
					        squeek_state_send_force_hidden(service->state_manager);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sm_puri_osk0_complete_set_visible(object, invocation);
 | 
					    sm_puri_osk0_complete_set_visible(object, invocation);
 | 
				
			||||||
@ -65,12 +67,12 @@ handle_set_visible(SmPuriOSK0 *object, GDBusMethodInvocation *invocation,
 | 
				
			|||||||
DBusHandler *
 | 
					DBusHandler *
 | 
				
			||||||
dbus_handler_new (GDBusConnection *connection,
 | 
					dbus_handler_new (GDBusConnection *connection,
 | 
				
			||||||
                      const gchar     *object_path,
 | 
					                      const gchar     *object_path,
 | 
				
			||||||
                  struct squeek_animation_visibility_manager *animman)
 | 
					                  struct squeek_state_manager *state_manager)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    DBusHandler *self = calloc(1, sizeof(DBusHandler));
 | 
					    DBusHandler *self = calloc(1, sizeof(DBusHandler));
 | 
				
			||||||
    self->object_path = g_strdup(object_path);
 | 
					    self->object_path = g_strdup(object_path);
 | 
				
			||||||
    self->connection = connection;
 | 
					    self->connection = connection;
 | 
				
			||||||
    self->animman = animman;
 | 
					    self->state_manager = state_manager;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    self->dbus_interface = sm_puri_osk0_skeleton_new();
 | 
					    self->dbus_interface = sm_puri_osk0_skeleton_new();
 | 
				
			||||||
    g_signal_connect(self->dbus_interface, "handle-set-visible",
 | 
					    g_signal_connect(self->dbus_interface, "handle-set-visible",
 | 
				
			||||||
 | 
				
			|||||||
@ -19,10 +19,11 @@
 | 
				
			|||||||
#ifndef DBUS_H_
 | 
					#ifndef DBUS_H_
 | 
				
			||||||
#define DBUS_H_ 1
 | 
					#define DBUS_H_ 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include "animation.h"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#include "sm.puri.OSK0.h"
 | 
					#include "sm.puri.OSK0.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// From main.h
 | 
				
			||||||
 | 
					struct squeek_state_manager;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
G_BEGIN_DECLS
 | 
					G_BEGIN_DECLS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#define DBUS_SERVICE_PATH "/sm/puri/OSK0"
 | 
					#define DBUS_SERVICE_PATH "/sm/puri/OSK0"
 | 
				
			||||||
@ -41,12 +42,12 @@ typedef struct _DBusHandler
 | 
				
			|||||||
    char *object_path;
 | 
					    char *object_path;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Forward incoming events there
 | 
					    /// Forward incoming events there
 | 
				
			||||||
    struct squeek_animation_visibility_manager *animman; // shared reference
 | 
					    struct squeek_state_manager *state_manager; // shared reference
 | 
				
			||||||
} DBusHandler;
 | 
					} DBusHandler;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
DBusHandler * dbus_handler_new      (GDBusConnection *connection,
 | 
					DBusHandler * dbus_handler_new      (GDBusConnection *connection,
 | 
				
			||||||
                                             const gchar     *object_path,
 | 
					                                             const gchar     *object_path,
 | 
				
			||||||
                                     struct squeek_animation_visibility_manager *animman);
 | 
					                                     struct squeek_state_manager *state_manager);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void dbus_handler_destroy(DBusHandler*);
 | 
					void dbus_handler_destroy(DBusHandler*);
 | 
				
			||||||
G_END_DECLS
 | 
					G_END_DECLS
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										141
									
								
								src/event_loop/driver.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								src/event_loop/driver.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,141 @@
 | 
				
			|||||||
 | 
					/* Copyright (C) 2021 Purism SPC
 | 
				
			||||||
 | 
					 * SPDX-License-Identifier: GPL-3.0+
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					/*! This drives the loop from the `loop` module.
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * The tracker loop needs to be driven somehow,
 | 
				
			||||||
 | 
					 * and connected to the external world,
 | 
				
			||||||
 | 
					 * both on the side of receiving and sending events.
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * That's going to be implementation-dependent,
 | 
				
			||||||
 | 
					 * connecting to some external mechanisms
 | 
				
			||||||
 | 
					 * for time, messages, and threading/callbacks.
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * This is the "imperative shell" part of the software,
 | 
				
			||||||
 | 
					 * and no longer unit-testable.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::event_loop;
 | 
				
			||||||
 | 
					use crate::logging;
 | 
				
			||||||
 | 
					use crate::main::Commands;
 | 
				
			||||||
 | 
					use crate::state::{ Application, Event };
 | 
				
			||||||
 | 
					use glib;
 | 
				
			||||||
 | 
					use std::sync::mpsc;
 | 
				
			||||||
 | 
					use std::thread;
 | 
				
			||||||
 | 
					use std::time::Instant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Traits
 | 
				
			||||||
 | 
					use crate::logging::Warn;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Sender = mpsc::Sender<Event>;
 | 
				
			||||||
 | 
					type UISender = glib::Sender<Commands>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// This loop driver spawns a new thread which updates the state in a loop,
 | 
				
			||||||
 | 
					/// in response to incoming events.
 | 
				
			||||||
 | 
					/// It sends outcomes to the glib main loop using a channel.
 | 
				
			||||||
 | 
					/// The outcomes are applied by the UI end of the channel in the `main` module.
 | 
				
			||||||
 | 
					// This could still be reasonably tested,
 | 
				
			||||||
 | 
					// by creating a glib::Sender and checking what messages it receives.
 | 
				
			||||||
 | 
					#[derive(Clone)]
 | 
				
			||||||
 | 
					pub struct Threaded {
 | 
				
			||||||
 | 
					    thread: Sender,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Threaded {
 | 
				
			||||||
 | 
					    pub fn new(ui: UISender, initial_state: Application) -> Self {
 | 
				
			||||||
 | 
					        let (sender, receiver) = mpsc::channel();
 | 
				
			||||||
 | 
					        let saved_sender = sender.clone();
 | 
				
			||||||
 | 
					        thread::spawn(move || {
 | 
				
			||||||
 | 
					            let mut state = event_loop::State::new(initial_state, Instant::now());
 | 
				
			||||||
 | 
					            loop {
 | 
				
			||||||
 | 
					                match receiver.recv() {
 | 
				
			||||||
 | 
					                    Ok(event) => {
 | 
				
			||||||
 | 
					                        state = Self::handle_loop_event(&sender, state, event, &ui);
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                    Err(e) => {
 | 
				
			||||||
 | 
					                        logging::print(logging::Level::Bug, &format!("Senders hung up, aborting: {}", e));
 | 
				
			||||||
 | 
					                        return;
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            thread: saved_sender,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    pub fn send(&self, event: Event) -> Result<(), mpsc::SendError<Event>> {
 | 
				
			||||||
 | 
					        self.thread.send(event)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    fn handle_loop_event(loop_sender: &Sender, state: event_loop::State, event: Event, ui: &UISender)
 | 
				
			||||||
 | 
					        -> event_loop::State
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        let now = Instant::now();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let (new_state, commands) = event_loop::handle_event(state.clone(), event, now);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ui.send(commands)
 | 
				
			||||||
 | 
					            .or_warn(&mut logging::Print, logging::Problem::Bug, "Can't send to UI");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if new_state.scheduled_wakeup != state.scheduled_wakeup {
 | 
				
			||||||
 | 
					            if let Some(when) = new_state.scheduled_wakeup {
 | 
				
			||||||
 | 
					                Self::schedule_timeout_wake(loop_sender, when);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        new_state
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn schedule_timeout_wake(loop_sender: &Sender, when: Instant) {
 | 
				
			||||||
 | 
					        let sender = loop_sender.clone();
 | 
				
			||||||
 | 
					        thread::spawn(move || {
 | 
				
			||||||
 | 
					            let now = Instant::now();
 | 
				
			||||||
 | 
					            thread::sleep(when - now);
 | 
				
			||||||
 | 
					            sender.send(Event::TimeoutReached(when))
 | 
				
			||||||
 | 
					                .or_warn(&mut logging::Print, logging::Problem::Warning, "Can't wake visibility manager");
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// For calling in only
 | 
				
			||||||
 | 
					mod c {
 | 
				
			||||||
 | 
					    use super::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    use crate::state::Presence;
 | 
				
			||||||
 | 
					    use crate::state::visibility;
 | 
				
			||||||
 | 
					    use crate::util::c::Wrapped;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    #[no_mangle]
 | 
				
			||||||
 | 
					    pub extern "C"
 | 
				
			||||||
 | 
					    fn squeek_state_send_force_visible(mgr: Wrapped<Threaded>) {
 | 
				
			||||||
 | 
					        let sender = mgr.clone_ref();
 | 
				
			||||||
 | 
					        let sender = sender.borrow();
 | 
				
			||||||
 | 
					        sender.send(Event::Visibility(visibility::Event::ForceVisible))
 | 
				
			||||||
 | 
					            .or_warn(&mut logging::Print, logging::Problem::Warning, "Can't send to state manager");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    #[no_mangle]
 | 
				
			||||||
 | 
					    pub extern "C"
 | 
				
			||||||
 | 
					    fn squeek_state_send_force_hidden(sender: Wrapped<Threaded>) {
 | 
				
			||||||
 | 
					        let sender = sender.clone_ref();
 | 
				
			||||||
 | 
					        let sender = sender.borrow();
 | 
				
			||||||
 | 
					        sender.send(Event::Visibility(visibility::Event::ForceHidden))
 | 
				
			||||||
 | 
					            .or_warn(&mut logging::Print, logging::Problem::Warning, "Can't send to state manager");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[no_mangle]
 | 
				
			||||||
 | 
					    pub extern "C"
 | 
				
			||||||
 | 
					    fn squeek_state_send_keyboard_present(sender: Wrapped<Threaded>, present: u32) {
 | 
				
			||||||
 | 
					        let sender = sender.clone_ref();
 | 
				
			||||||
 | 
					        let sender = sender.borrow();
 | 
				
			||||||
 | 
					        let state =
 | 
				
			||||||
 | 
					            if present == 0 { Presence::Missing }
 | 
				
			||||||
 | 
					            else { Presence::Present };
 | 
				
			||||||
 | 
					        sender.send(Event::PhysicalKeyboard(state))
 | 
				
			||||||
 | 
					            .or_warn(&mut logging::Print, logging::Problem::Warning, "Can't send to state manager");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										186
									
								
								src/event_loop/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										186
									
								
								src/event_loop/mod.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,186 @@
 | 
				
			|||||||
 | 
					/* Copyright (C) 2021 Purism SPC
 | 
				
			||||||
 | 
					 * SPDX-License-Identifier: GPL-3.0+
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*! The loop abstraction for driving state changes.
 | 
				
			||||||
 | 
					 * It binds to the state tracker in `state::Application`,
 | 
				
			||||||
 | 
					 * and actually gets driven by a driver in the `driver` module.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * * * *
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * If we performed updates in a tight loop,
 | 
				
			||||||
 | 
					 * the state tracker would have been all we need.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * ``
 | 
				
			||||||
 | 
					 * loop {
 | 
				
			||||||
 | 
					 *  event = current_event()
 | 
				
			||||||
 | 
					 *  outcome = update_state(event)
 | 
				
			||||||
 | 
					 *  io.apply(outcome)
 | 
				
			||||||
 | 
					 * }
 | 
				
			||||||
 | 
					 * ``
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * This is enough to process all events,
 | 
				
			||||||
 | 
					 * and keep the window always in sync with the current state.
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * However, we're trying to be conservative,
 | 
				
			||||||
 | 
					 * and not waste time performing updates that don't change state,
 | 
				
			||||||
 | 
					 * so we have to react to events that end up influencing the state.
 | 
				
			||||||
 | 
					 * 
 | 
				
			||||||
 | 
					 * One complication from that is that animation steps
 | 
				
			||||||
 | 
					 * are not a response to events coming from the owner of the loop,
 | 
				
			||||||
 | 
					 * but are needed by the loop itself.
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 * This is where the rest of bugs hide:
 | 
				
			||||||
 | 
					 * too few scheduled wakeups mean missed updates and wrong visible state.
 | 
				
			||||||
 | 
					 * Too many wakeups can slow down the process, or make animation jittery.
 | 
				
			||||||
 | 
					 * The loop iteration is kept as a pure function to stay testable.
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod driver;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// This module is tightly coupled to the shape of data passed around in this project.
 | 
				
			||||||
 | 
					// That's not a problem as long as there's only one loop.
 | 
				
			||||||
 | 
					// They can still be abstracted into Traits,
 | 
				
			||||||
 | 
					// and the loop parametrized over them.
 | 
				
			||||||
 | 
					use crate::main::Commands;
 | 
				
			||||||
 | 
					use crate::state;
 | 
				
			||||||
 | 
					use crate::state::Event;
 | 
				
			||||||
 | 
					use std::cmp;
 | 
				
			||||||
 | 
					use std::time::{ Duration, Instant };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// This keeps the state of the tracker loop between iterations
 | 
				
			||||||
 | 
					#[derive(Clone)]
 | 
				
			||||||
 | 
					struct State {
 | 
				
			||||||
 | 
					    state: state::Application,
 | 
				
			||||||
 | 
					    scheduled_wakeup: Option<Instant>,
 | 
				
			||||||
 | 
					    last_update: Instant,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl State {
 | 
				
			||||||
 | 
					    fn new(initial_state: state::Application, now: Instant) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            state: initial_state,
 | 
				
			||||||
 | 
					            scheduled_wakeup: None,
 | 
				
			||||||
 | 
					            last_update: now,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A single iteration of the loop, updating its persistent state.
 | 
				
			||||||
 | 
					/// - updates tracker state,
 | 
				
			||||||
 | 
					/// - determines outcome,
 | 
				
			||||||
 | 
					/// - determines next scheduled animation wakeup,
 | 
				
			||||||
 | 
					/// and because this is a pure function, it's easily testable.
 | 
				
			||||||
 | 
					/// It returns the new state, and the message to send onwards.
 | 
				
			||||||
 | 
					fn handle_event(
 | 
				
			||||||
 | 
					    mut loop_state: State,
 | 
				
			||||||
 | 
					    event: Event,
 | 
				
			||||||
 | 
					    now: Instant,
 | 
				
			||||||
 | 
					) -> (State, Commands) {
 | 
				
			||||||
 | 
					    // Calculate changes to send to the consumer,
 | 
				
			||||||
 | 
					    // based on publicly visible state.
 | 
				
			||||||
 | 
					    // The internal state may change more often than the publicly visible one,
 | 
				
			||||||
 | 
					    // so the resulting changes may be no-ops.
 | 
				
			||||||
 | 
					    let old_state = loop_state.state.clone();
 | 
				
			||||||
 | 
					    let last_update = loop_state.last_update;
 | 
				
			||||||
 | 
					    loop_state.state = loop_state.state.apply_event(event.clone(), now);
 | 
				
			||||||
 | 
					    loop_state.last_update = now;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let new_outcome = loop_state.state.get_outcome(now);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let commands = old_state.get_outcome(last_update)
 | 
				
			||||||
 | 
					        .get_commands_to_reach(&new_outcome);
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Timeout events are special: they affect the scheduled timeout.
 | 
				
			||||||
 | 
					    loop_state.scheduled_wakeup = match event {
 | 
				
			||||||
 | 
					        Event::TimeoutReached(when) => {
 | 
				
			||||||
 | 
					            if when > now {
 | 
				
			||||||
 | 
					                // Special handling for scheduled events coming in early.
 | 
				
			||||||
 | 
					                // Wait at least 10 ms to avoid Zeno's paradox.
 | 
				
			||||||
 | 
					                // This is probably not needed though,
 | 
				
			||||||
 | 
					                // if the `now` contains the desired time of the event.
 | 
				
			||||||
 | 
					                // But then what about time "reversing"?
 | 
				
			||||||
 | 
					                Some(cmp::max(
 | 
				
			||||||
 | 
					                    when,
 | 
				
			||||||
 | 
					                    now + Duration::from_millis(10),
 | 
				
			||||||
 | 
					                ))
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                // There's only one timeout in flight, and it's this one.
 | 
				
			||||||
 | 
					                // It's about to complete, and then the tracker can be cleared.
 | 
				
			||||||
 | 
					                // I'm not sure if this is strictly necessary.
 | 
				
			||||||
 | 
					                None
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        _ => loop_state.scheduled_wakeup.clone(),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Reschedule timeout if the new state calls for it.
 | 
				
			||||||
 | 
					    let scheduled = &loop_state.scheduled_wakeup;
 | 
				
			||||||
 | 
					    let desired = loop_state.state.get_next_wake(now);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    loop_state.scheduled_wakeup = match (scheduled, desired) {
 | 
				
			||||||
 | 
					        (&Some(scheduled), Some(next)) => {
 | 
				
			||||||
 | 
					            if scheduled > next {
 | 
				
			||||||
 | 
					                // State wants a wake to happen before the one which is already scheduled.
 | 
				
			||||||
 | 
					                // The previous state is removed in order to only ever keep one in flight.
 | 
				
			||||||
 | 
					                // That hopefully avoids pileups,
 | 
				
			||||||
 | 
					                // e.g. because the system is busy
 | 
				
			||||||
 | 
					                // and the user keeps doing something that queues more events.
 | 
				
			||||||
 | 
					                Some(next)
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                // Not changing the case when the wanted wake is *after* scheduled,
 | 
				
			||||||
 | 
					                // because wakes are not expensive as long as they don't pile up,
 | 
				
			||||||
 | 
					                // and I can't see a pileup potential when it doesn't retrigger itself.
 | 
				
			||||||
 | 
					                // Skipping an expected event is much more dangerous.
 | 
				
			||||||
 | 
					                Some(scheduled)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        (None, Some(next)) => Some(next),
 | 
				
			||||||
 | 
					        // No need to change the unneeded wake - see above.
 | 
				
			||||||
 | 
					        // (Some(_), None) => ...
 | 
				
			||||||
 | 
					        (other, _) => other.clone(),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    (loop_state, commands)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(test)]
 | 
				
			||||||
 | 
					mod test {
 | 
				
			||||||
 | 
					    use super::*;
 | 
				
			||||||
 | 
					    use crate::animation;
 | 
				
			||||||
 | 
					    use crate::imservice::{ ContentHint, ContentPurpose };
 | 
				
			||||||
 | 
					    use crate::main::PanelCommand;
 | 
				
			||||||
 | 
					    use crate::state::{ Application, InputMethod, InputMethodDetails, Presence, visibility };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn imdetails_new() -> InputMethodDetails {
 | 
				
			||||||
 | 
					        InputMethodDetails {
 | 
				
			||||||
 | 
					            purpose: ContentPurpose::Normal,
 | 
				
			||||||
 | 
					            hint: ContentHint::NONE,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn schedule_hide() {
 | 
				
			||||||
 | 
					        let start = Instant::now(); // doesn't matter when. It would be better to have a reproducible value though
 | 
				
			||||||
 | 
					        let mut now = start;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let state = Application {
 | 
				
			||||||
 | 
					            im: InputMethod::Active(imdetails_new()),
 | 
				
			||||||
 | 
					            physical_keyboard: Presence::Missing,
 | 
				
			||||||
 | 
					            visibility_override: visibility::State::NotForced,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        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_eq!(l.scheduled_wakeup, Some(now + animation::HIDING_TIMEOUT));
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        now += animation::HIDING_TIMEOUT;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        let (l, commands) = handle_event(l, Event::TimeoutReached(now), now);
 | 
				
			||||||
 | 
					        assert_eq!(commands.panel_visibility, Some(PanelCommand::Hide));
 | 
				
			||||||
 | 
					        assert_eq!(l.scheduled_wakeup, None);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -23,22 +23,6 @@ static const struct zwp_input_method_v2_listener input_method_listener = {
 | 
				
			|||||||
    .unavailable = imservice_handle_unavailable,
 | 
					    .unavailable = imservice_handle_unavailable,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
struct submission* get_submission(struct zwp_input_method_manager_v2 *immanager,
 | 
					 | 
				
			||||||
                                  struct zwp_virtual_keyboard_manager_v1 *vkmanager,
 | 
					 | 
				
			||||||
                                  struct vis_manager *vis_manager,
 | 
					 | 
				
			||||||
                                  struct wl_seat *seat,
 | 
					 | 
				
			||||||
                                  EekboardContextService *state) {
 | 
					 | 
				
			||||||
    struct zwp_input_method_v2 *im = NULL;
 | 
					 | 
				
			||||||
    if (immanager) {
 | 
					 | 
				
			||||||
        im = zwp_input_method_manager_v2_get_input_method(immanager, seat);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    struct zwp_virtual_keyboard_v1 *vk = NULL;
 | 
					 | 
				
			||||||
    if (vkmanager) {
 | 
					 | 
				
			||||||
        vk = zwp_virtual_keyboard_manager_v1_create_virtual_keyboard(vkmanager, seat);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return submission_new(im, vk, state, vis_manager);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Un-inlined
 | 
					/// Un-inlined
 | 
				
			||||||
struct zwp_input_method_v2 *imservice_manager_get_input_method(struct zwp_input_method_manager_v2 *manager,
 | 
					struct zwp_input_method_v2 *imservice_manager_get_input_method(struct zwp_input_method_manager_v2 *manager,
 | 
				
			||||||
                                   struct wl_seat *seat) {
 | 
					                                   struct wl_seat *seat) {
 | 
				
			||||||
 | 
				
			|||||||
@ -8,7 +8,11 @@ use std::ffi::CString;
 | 
				
			|||||||
use std::fmt;
 | 
					use std::fmt;
 | 
				
			||||||
use std::num::Wrapping;
 | 
					use std::num::Wrapping;
 | 
				
			||||||
use std::string::String;
 | 
					use std::string::String;
 | 
				
			||||||
 | 
					use std::time::Instant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::event_loop::driver;
 | 
				
			||||||
 | 
					use crate::state;
 | 
				
			||||||
 | 
					use crate::state::Event;
 | 
				
			||||||
use ::logging;
 | 
					use ::logging;
 | 
				
			||||||
use ::util::c::into_cstring;
 | 
					use ::util::c::into_cstring;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -23,8 +27,6 @@ pub mod c {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    use std::os::raw::{c_char, c_void};
 | 
					    use std::os::raw::{c_char, c_void};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub use ::submission::c::StateManager;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // The following defined in C
 | 
					    // The following defined in C
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
    /// struct zwp_input_method_v2*
 | 
					    /// struct zwp_input_method_v2*
 | 
				
			||||||
@ -39,7 +41,6 @@ pub mod c {
 | 
				
			|||||||
        pub fn eek_input_method_commit_string(im: *mut InputMethod, text: *const c_char);
 | 
					        pub fn eek_input_method_commit_string(im: *mut InputMethod, text: *const c_char);
 | 
				
			||||||
        pub fn eek_input_method_delete_surrounding_text(im: *mut InputMethod, before: u32, after: u32);
 | 
					        pub fn eek_input_method_delete_surrounding_text(im: *mut InputMethod, before: u32, after: u32);
 | 
				
			||||||
        pub fn eek_input_method_commit(im: *mut InputMethod, serial: u32);
 | 
					        pub fn eek_input_method_commit(im: *mut InputMethod, serial: u32);
 | 
				
			||||||
        fn eekboard_context_service_set_hint_purpose(state: *const StateManager, hint: u32, purpose: u32);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // The following defined in Rust. TODO: wrap naked pointers to Rust data inside RefCells to prevent multiple writers
 | 
					    // The following defined in Rust. TODO: wrap naked pointers to Rust data inside RefCells to prevent multiple writers
 | 
				
			||||||
@ -140,7 +141,6 @@ pub mod c {
 | 
				
			|||||||
        im: *const InputMethod)
 | 
					        im: *const InputMethod)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        let imservice = check_imservice(imservice, im).unwrap();
 | 
					        let imservice = check_imservice(imservice, im).unwrap();
 | 
				
			||||||
        let active_changed = imservice.current.active ^ imservice.pending.active;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        imservice.current = imservice.pending.clone();
 | 
					        imservice.current = imservice.pending.clone();
 | 
				
			||||||
        imservice.pending = IMProtocolState {
 | 
					        imservice.pending = IMProtocolState {
 | 
				
			||||||
@ -149,19 +149,7 @@ pub mod c {
 | 
				
			|||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        imservice.serial += Wrapping(1u32);
 | 
					        imservice.serial += Wrapping(1u32);
 | 
				
			||||||
 | 
					        imservice.send_event();
 | 
				
			||||||
        if active_changed {
 | 
					 | 
				
			||||||
            (imservice.active_callback)(imservice.current.active);
 | 
					 | 
				
			||||||
            if imservice.current.active {
 | 
					 | 
				
			||||||
                unsafe {
 | 
					 | 
				
			||||||
                    eekboard_context_service_set_hint_purpose(
 | 
					 | 
				
			||||||
                        imservice.state_manager,
 | 
					 | 
				
			||||||
                        imservice.current.content_hint.bits(),
 | 
					 | 
				
			||||||
                        imservice.current.content_purpose.clone() as u32,
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // TODO: this is really untested
 | 
					    // TODO: this is really untested
 | 
				
			||||||
@ -177,7 +165,7 @@ pub mod c {
 | 
				
			|||||||
        // the keyboard is already decommissioned
 | 
					        // the keyboard is already decommissioned
 | 
				
			||||||
        imservice.current.active = false;
 | 
					        imservice.current.active = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        (imservice.active_callback)(imservice.current.active);
 | 
					        imservice.send_event();
 | 
				
			||||||
    }    
 | 
					    }    
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // FIXME: destroy and deallocate
 | 
					    // FIXME: destroy and deallocate
 | 
				
			||||||
@ -328,9 +316,7 @@ impl Default for IMProtocolState {
 | 
				
			|||||||
pub struct IMService {
 | 
					pub struct IMService {
 | 
				
			||||||
    /// Owned reference (still created and destroyed in C)
 | 
					    /// Owned reference (still created and destroyed in C)
 | 
				
			||||||
    pub im: *mut c::InputMethod,
 | 
					    pub im: *mut c::InputMethod,
 | 
				
			||||||
    /// Unowned reference. Be careful, it's shared with C at large
 | 
					    sender: driver::Threaded,
 | 
				
			||||||
    state_manager: *const c::StateManager,
 | 
					 | 
				
			||||||
    active_callback: Box<dyn Fn(bool)>,
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pending: IMProtocolState,
 | 
					    pending: IMProtocolState,
 | 
				
			||||||
    current: IMProtocolState, // turn current into an idiomatic representation?
 | 
					    current: IMProtocolState, // turn current into an idiomatic representation?
 | 
				
			||||||
@ -346,15 +332,13 @@ pub enum SubmitError {
 | 
				
			|||||||
impl IMService {
 | 
					impl IMService {
 | 
				
			||||||
    pub fn new(
 | 
					    pub fn new(
 | 
				
			||||||
        im: *mut c::InputMethod,
 | 
					        im: *mut c::InputMethod,
 | 
				
			||||||
        state_manager: *const c::StateManager,
 | 
					        sender: driver::Threaded,
 | 
				
			||||||
        active_callback: Box<dyn Fn(bool)>,
 | 
					 | 
				
			||||||
    ) -> Box<IMService> {
 | 
					    ) -> Box<IMService> {
 | 
				
			||||||
        // IMService will be referenced to by C,
 | 
					        // IMService will be referenced to by C,
 | 
				
			||||||
        // so it needs to stay in the same place in memory via Box
 | 
					        // so it needs to stay in the same place in memory via Box
 | 
				
			||||||
        let imservice = Box::new(IMService {
 | 
					        let imservice = Box::new(IMService {
 | 
				
			||||||
            im,
 | 
					            im,
 | 
				
			||||||
            active_callback,
 | 
					            sender,
 | 
				
			||||||
            state_manager,
 | 
					 | 
				
			||||||
            pending: IMProtocolState::default(),
 | 
					            pending: IMProtocolState::default(),
 | 
				
			||||||
            current: IMProtocolState::default(),
 | 
					            current: IMProtocolState::default(),
 | 
				
			||||||
            preedit_string: String::new(),
 | 
					            preedit_string: String::new(),
 | 
				
			||||||
@ -414,4 +398,21 @@ impl IMService {
 | 
				
			|||||||
    pub fn is_active(&self) -> bool {
 | 
					    pub fn is_active(&self) -> bool {
 | 
				
			||||||
        self.current.active
 | 
					        self.current.active
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn send_event(&self) {
 | 
				
			||||||
 | 
					        let state = &self.current;
 | 
				
			||||||
 | 
					        let timestamp = Instant::now();
 | 
				
			||||||
 | 
					        let message = if state.active {
 | 
				
			||||||
 | 
					            state::InputMethod::Active(
 | 
				
			||||||
 | 
					                state::InputMethodDetails {
 | 
				
			||||||
 | 
					                    hint: state.content_hint,
 | 
				
			||||||
 | 
					                    purpose: state.content_purpose,
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            state::InputMethod::InactiveSince(timestamp)
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        self.sender.send(Event::InputMethod(message))
 | 
				
			||||||
 | 
					            .or_warn(&mut logging::Print, logging::Problem::Warning, "Can't send to state manager");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -22,6 +22,7 @@ mod action;
 | 
				
			|||||||
mod animation;
 | 
					mod animation;
 | 
				
			||||||
pub mod data;
 | 
					pub mod data;
 | 
				
			||||||
mod drawing;
 | 
					mod drawing;
 | 
				
			||||||
 | 
					mod event_loop;
 | 
				
			||||||
pub mod float_ord;
 | 
					pub mod float_ord;
 | 
				
			||||||
pub mod imservice;
 | 
					pub mod imservice;
 | 
				
			||||||
mod keyboard;
 | 
					mod keyboard;
 | 
				
			||||||
@ -32,6 +33,7 @@ mod manager;
 | 
				
			|||||||
mod outputs;
 | 
					mod outputs;
 | 
				
			||||||
mod popover;
 | 
					mod popover;
 | 
				
			||||||
mod resources;
 | 
					mod resources;
 | 
				
			||||||
 | 
					mod state;
 | 
				
			||||||
mod style;
 | 
					mod style;
 | 
				
			||||||
mod submission;
 | 
					mod submission;
 | 
				
			||||||
pub mod tests;
 | 
					pub mod tests;
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										28
									
								
								src/main.h
									
									
									
									
									
								
							
							
						
						
									
										28
									
								
								src/main.h
									
									
									
									
									
								
							@ -1,17 +1,33 @@
 | 
				
			|||||||
#pragma once
 | 
					#pragma once
 | 
				
			||||||
/// This all wraps https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/struct.MainContext.html#method.channel
 | 
					/// This all wraps https://gtk-rs.org/gtk-rs-core/stable/latest/docs/glib/struct.MainContext.html#method.channel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <inttypes.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "input-method-unstable-v2-client-protocol.h"
 | 
				
			||||||
 | 
					#include "virtual-keyboard-unstable-v1-client-protocol.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include "eek/eek-types.h"
 | 
					#include "eek/eek-types.h"
 | 
				
			||||||
#include "dbus.h"
 | 
					#include "dbus.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
struct receiver;
 | 
					 | 
				
			||||||
struct sender;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
struct channel {
 | 
					struct receiver;
 | 
				
			||||||
    struct sender *sender;
 | 
					
 | 
				
			||||||
 | 
					/// Wrapped<event_loop::driver::Threaded>
 | 
				
			||||||
 | 
					struct squeek_state_manager;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct submission;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct rsobjects {
 | 
				
			||||||
    struct receiver *receiver;
 | 
					    struct receiver *receiver;
 | 
				
			||||||
 | 
					    struct squeek_state_manager *state_manager;
 | 
				
			||||||
 | 
					    struct submission *submission;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Creates a channel with one end inside the glib main loop
 | 
					 | 
				
			||||||
struct channel main_loop_channel_new(void);
 | 
					 | 
				
			||||||
void register_ui_loop_handler(struct receiver *receiver, ServerContextService *ui, DBusHandler *dbus_handler);
 | 
					void register_ui_loop_handler(struct receiver *receiver, ServerContextService *ui, DBusHandler *dbus_handler);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct rsobjects squeek_rsobjects_new(struct zwp_input_method_v2 *im, struct zwp_virtual_keyboard_v1 *vk);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void squeek_state_send_force_visible(struct squeek_state_manager *state);
 | 
				
			||||||
 | 
					void squeek_state_send_force_hidden(struct squeek_state_manager *state);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void squeek_state_send_keyboard_present(struct squeek_state_manager *state, uint32_t keyboard_present);
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										124
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						
									
										124
									
								
								src/main.rs
									
									
									
									
									
								
							@ -4,17 +4,23 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
/*! Glue for the main loop. */
 | 
					/*! Glue for the main loop. */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::animation::Outcome as Message;
 | 
					use crate::state;
 | 
				
			||||||
use glib::{Continue, MainContext, PRIORITY_DEFAULT, Receiver, Sender};
 | 
					use glib::{Continue, MainContext, PRIORITY_DEFAULT, Receiver};
 | 
				
			||||||
use std::thread;
 | 
					
 | 
				
			||||||
use std::time::Duration;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
mod c {
 | 
					mod c {
 | 
				
			||||||
    use super::*;
 | 
					    use super::*;
 | 
				
			||||||
    use std::os::raw::c_void;
 | 
					    use std::os::raw::c_void;
 | 
				
			||||||
    use std::rc::Rc;
 | 
					    use std::rc::Rc;
 | 
				
			||||||
 | 
					    use std::time::Instant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    use ::util::c::{ ArcWrapped, Wrapped };
 | 
					    use crate::event_loop::driver;
 | 
				
			||||||
 | 
					    use crate::imservice::IMService;
 | 
				
			||||||
 | 
					    use crate::imservice::c::InputMethod;
 | 
				
			||||||
 | 
					    use crate::state;
 | 
				
			||||||
 | 
					    use crate::submission::Submission;
 | 
				
			||||||
 | 
					    use crate::util::c::Wrapped;
 | 
				
			||||||
 | 
					    use crate::vkeyboard::c::ZwpVirtualKeyboardV1;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    /// ServerContextService*
 | 
					    /// ServerContextService*
 | 
				
			||||||
    #[repr(transparent)]
 | 
					    #[repr(transparent)]
 | 
				
			||||||
@ -24,56 +30,56 @@ mod c {
 | 
				
			|||||||
    #[repr(transparent)]
 | 
					    #[repr(transparent)]
 | 
				
			||||||
    pub struct DBusHandler(*const c_void);
 | 
					    pub struct DBusHandler(*const c_void);
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    /// Corresponds to main.c::channel
 | 
					    /// Holds the Rust structures that are interesting from C.
 | 
				
			||||||
    #[repr(C)]
 | 
					    #[repr(C)]
 | 
				
			||||||
    pub struct Channel {
 | 
					    pub struct RsObjects {
 | 
				
			||||||
        sender: ArcWrapped<Sender<Message>>,
 | 
					        receiver: Wrapped<Receiver<Commands>>,
 | 
				
			||||||
        receiver: Wrapped<Receiver<Message>>,
 | 
					        state_manager: Wrapped<driver::Threaded>,
 | 
				
			||||||
 | 
					        submission: Wrapped<Submission>,
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    extern "C" {
 | 
					    extern "C" {
 | 
				
			||||||
        pub fn server_context_service_real_show_keyboard(imservice: *const UIManager);
 | 
					        fn server_context_service_real_show_keyboard(service: *const UIManager);
 | 
				
			||||||
        pub fn server_context_service_real_hide_keyboard(imservice: *const UIManager);
 | 
					        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,
 | 
					        // This should probably only get called from the gtk main loop,
 | 
				
			||||||
        // given that dbus handler is using glib.
 | 
					        // given that dbus handler is using glib.
 | 
				
			||||||
        pub fn dbus_handler_set_visible(dbus: *const DBusHandler, visible: u8);
 | 
					        fn dbus_handler_set_visible(dbus: *const DBusHandler, visible: u8);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
 | 
					    /// Creates what's possible in Rust to eliminate as many FFI calls as possible,
 | 
				
			||||||
 | 
					    /// because types aren't getting checked across their boundaries,
 | 
				
			||||||
 | 
					    /// and that leads to suffering.
 | 
				
			||||||
    #[no_mangle]
 | 
					    #[no_mangle]
 | 
				
			||||||
    pub extern "C"
 | 
					    pub extern "C"
 | 
				
			||||||
    fn main_loop_channel_new() -> Channel {
 | 
					    fn squeek_rsobjects_new(
 | 
				
			||||||
 | 
					        im: *mut InputMethod,
 | 
				
			||||||
 | 
					        vk: ZwpVirtualKeyboardV1,
 | 
				
			||||||
 | 
					    ) -> RsObjects {
 | 
				
			||||||
        let (sender, receiver) = MainContext::channel(PRIORITY_DEFAULT);
 | 
					        let (sender, receiver) = MainContext::channel(PRIORITY_DEFAULT);
 | 
				
			||||||
        let sender = ArcWrapped::new(sender);
 | 
					        
 | 
				
			||||||
        let receiver = Wrapped::new(receiver);
 | 
					        let now = Instant::now();
 | 
				
			||||||
        let channel = Channel {
 | 
					        let state_manager = driver::Threaded::new(sender, state::Application::new(now));
 | 
				
			||||||
            sender,
 | 
					
 | 
				
			||||||
            receiver,
 | 
					        let imservice = if im.is_null() {
 | 
				
			||||||
 | 
					            None
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            Some(IMService::new(im, state_manager.clone()))
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					        let submission = Submission::new(vk, imservice);
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        //start_work(channel.sender.clone());
 | 
					        RsObjects {
 | 
				
			||||||
        
 | 
					            submission: Wrapped::new(submission),
 | 
				
			||||||
        channel
 | 
					            state_manager: Wrapped::new(state_manager),
 | 
				
			||||||
    }
 | 
					            receiver: Wrapped::new(receiver),
 | 
				
			||||||
    
 | 
					        }
 | 
				
			||||||
    /// testing only
 | 
					 | 
				
			||||||
    fn start_work(sender: ArcWrapped<Sender<Message>>) {
 | 
					 | 
				
			||||||
        let sender = sender.clone_ref();
 | 
					 | 
				
			||||||
        thread::spawn(move || {
 | 
					 | 
				
			||||||
            let sender = sender.lock().unwrap();
 | 
					 | 
				
			||||||
            thread::sleep(Duration::from_secs(3));
 | 
					 | 
				
			||||||
            sender.send(Message::Visible).unwrap();
 | 
					 | 
				
			||||||
            thread::sleep(Duration::from_secs(3));
 | 
					 | 
				
			||||||
            sender.send(Message::Hidden).unwrap();
 | 
					 | 
				
			||||||
            thread::sleep(Duration::from_secs(3));
 | 
					 | 
				
			||||||
            sender.send(Message::Visible).unwrap();
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Places the UI loop callback in the glib main loop.
 | 
					    /// Places the UI loop callback in the glib main loop.
 | 
				
			||||||
    #[no_mangle]
 | 
					    #[no_mangle]
 | 
				
			||||||
    pub extern "C"
 | 
					    pub extern "C"
 | 
				
			||||||
    fn register_ui_loop_handler(
 | 
					    fn register_ui_loop_handler(
 | 
				
			||||||
        receiver: Wrapped<Receiver<Message>>,
 | 
					        receiver: Wrapped<Receiver<Commands>>,
 | 
				
			||||||
        ui_manager: *const UIManager,
 | 
					        ui_manager: *const UIManager,
 | 
				
			||||||
        dbus_handler: *const DBusHandler,
 | 
					        dbus_handler: *const DBusHandler,
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
@ -97,23 +103,47 @@ mod c {
 | 
				
			|||||||
    /// This is the outest layer of the imperative shell,
 | 
					    /// This is the outest layer of the imperative shell,
 | 
				
			||||||
    /// and doesn't lend itself to testing other than integration.
 | 
					    /// and doesn't lend itself to testing other than integration.
 | 
				
			||||||
    fn main_loop_handle_message(
 | 
					    fn main_loop_handle_message(
 | 
				
			||||||
        msg: Message,
 | 
					        msg: Commands,
 | 
				
			||||||
        ui_manager: *const UIManager,
 | 
					        ui_manager: *const UIManager,
 | 
				
			||||||
        dbus_handler: *const DBusHandler,
 | 
					        dbus_handler: *const DBusHandler,
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
        match msg {
 | 
					        match msg.panel_visibility {
 | 
				
			||||||
            Message::Visible => unsafe {
 | 
					            Some(PanelCommand::Show) => unsafe {
 | 
				
			||||||
                // FIXME: reset layout to default if no IM field is active
 | 
					 | 
				
			||||||
                // Ideally: anim state stores the current IM hints,
 | 
					 | 
				
			||||||
                // Message::Visible(hints) is received here
 | 
					 | 
				
			||||||
                // and applied to layout
 | 
					 | 
				
			||||||
                server_context_service_real_show_keyboard(ui_manager);
 | 
					                server_context_service_real_show_keyboard(ui_manager);
 | 
				
			||||||
                dbus_handler_set_visible(dbus_handler, 1);
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            Message::Hidden => unsafe {
 | 
					            Some(PanelCommand::Hide) => unsafe {
 | 
				
			||||||
                server_context_service_real_hide_keyboard(ui_manager);
 | 
					                server_context_service_real_hide_keyboard(ui_manager);
 | 
				
			||||||
                dbus_handler_set_visible(dbus_handler, 0);
 | 
					 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
 | 
					            None => {},
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Some(visible) = msg.dbus_visible_set {
 | 
				
			||||||
 | 
					            unsafe { dbus_handler_set_visible(dbus_handler, visible as u8) };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let Some(hints) = msg.layout_hint_set {
 | 
				
			||||||
 | 
					            unsafe {
 | 
				
			||||||
 | 
					                server_context_service_set_hint_purpose(
 | 
				
			||||||
 | 
					                    ui_manager,
 | 
				
			||||||
 | 
					                    hints.hint.bits(),
 | 
				
			||||||
 | 
					                    hints.purpose.clone() as u32,
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone, PartialEq, Debug)]
 | 
				
			||||||
 | 
					pub enum PanelCommand {
 | 
				
			||||||
 | 
					    Show,
 | 
				
			||||||
 | 
					    Hide,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// The commands consumed by the main loop,
 | 
				
			||||||
 | 
					/// to be sent out to external components.
 | 
				
			||||||
 | 
					#[derive(Clone)]
 | 
				
			||||||
 | 
					pub struct Commands {
 | 
				
			||||||
 | 
					    pub panel_visibility: Option<PanelCommand>,
 | 
				
			||||||
 | 
					    pub layout_hint_set: Option<state::InputMethodDetails>,
 | 
				
			||||||
 | 
					    pub dbus_visible_set: Option<bool>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -42,7 +42,7 @@ struct _ServerContextService {
 | 
				
			|||||||
    struct submission *submission; // unowned
 | 
					    struct submission *submission; // unowned
 | 
				
			||||||
    struct squeek_layout_state *layout;
 | 
					    struct squeek_layout_state *layout;
 | 
				
			||||||
    struct ui_manager *manager; // unowned
 | 
					    struct ui_manager *manager; // unowned
 | 
				
			||||||
    struct vis_manager *vis_manager; // owned
 | 
					    struct squeek_state_manager *state_manager; // shared reference
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    PhoshLayerSurface *window;
 | 
					    PhoshLayerSurface *window;
 | 
				
			||||||
    GtkWidget *widget; // nullable
 | 
					    GtkWidget *widget; // nullable
 | 
				
			||||||
@ -203,6 +203,7 @@ make_widget (ServerContextService *self)
 | 
				
			|||||||
    gtk_widget_show_all(self->widget);
 | 
					    gtk_widget_show_all(self->widget);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Called from rust
 | 
				
			||||||
void
 | 
					void
 | 
				
			||||||
server_context_service_real_show_keyboard (ServerContextService *self)
 | 
					server_context_service_real_show_keyboard (ServerContextService *self)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@ -215,17 +216,13 @@ server_context_service_real_show_keyboard (ServerContextService *self)
 | 
				
			|||||||
    gtk_widget_show (GTK_WIDGET(self->window));
 | 
					    gtk_widget_show (GTK_WIDGET(self->window));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Called from rust
 | 
				
			||||||
void
 | 
					void
 | 
				
			||||||
server_context_service_real_hide_keyboard (ServerContextService *self)
 | 
					server_context_service_real_hide_keyboard (ServerContextService *self)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    gtk_widget_hide (GTK_WIDGET(self->window));
 | 
					    if (self->window) {
 | 
				
			||||||
}
 | 
						    gtk_widget_hide (GTK_WIDGET(self->window));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
server_context_service_set_physical_keyboard_present (ServerContextService *self, gboolean physical_keyboard_present)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    g_return_if_fail (SERVER_IS_CONTEXT_SERVICE (self));
 | 
					 | 
				
			||||||
    squeek_visman_set_keyboard_present(self->vis_manager, physical_keyboard_present);
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static void
 | 
					static void
 | 
				
			||||||
@ -238,7 +235,7 @@ server_context_service_set_property (GObject      *object,
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    switch (prop_id) {
 | 
					    switch (prop_id) {
 | 
				
			||||||
    case PROP_ENABLED:
 | 
					    case PROP_ENABLED:
 | 
				
			||||||
        server_context_service_set_physical_keyboard_present (self, !g_value_get_boolean (value));
 | 
					        squeek_state_send_keyboard_present(self->state_manager, !g_value_get_boolean (value));
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
    default:
 | 
					    default:
 | 
				
			||||||
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
 | 
					        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
 | 
				
			||||||
@ -321,14 +318,20 @@ init (ServerContextService *self) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ServerContextService *
 | 
					ServerContextService *
 | 
				
			||||||
server_context_service_new (EekboardContextService *self, struct submission *submission, struct squeek_layout_state *layout, struct ui_manager *uiman, struct vis_manager *visman)
 | 
					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 *ui = g_object_new (SERVER_TYPE_CONTEXT_SERVICE, NULL);
 | 
					    ServerContextService *ui = g_object_new (SERVER_TYPE_CONTEXT_SERVICE, NULL);
 | 
				
			||||||
    ui->submission = submission;
 | 
					    ui->submission = submission;
 | 
				
			||||||
    ui->state = self;
 | 
					    ui->state = self;
 | 
				
			||||||
    ui->layout = layout;
 | 
					    ui->layout = layout;
 | 
				
			||||||
    ui->manager = uiman;
 | 
					    ui->manager = uiman;
 | 
				
			||||||
    ui->vis_manager = visman;
 | 
					    ui->state_manager = state_manager;
 | 
				
			||||||
    init(ui);
 | 
					    init(ui);
 | 
				
			||||||
    return ui;
 | 
					    return ui;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Used from Rust
 | 
				
			||||||
 | 
					void server_context_service_set_hint_purpose(ServerContextService *self, uint32_t hint,
 | 
				
			||||||
 | 
					                                              uint32_t purpose) {
 | 
				
			||||||
 | 
					    eekboard_context_service_set_hint_purpose(self->state, hint, purpose);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -29,7 +29,7 @@ G_BEGIN_DECLS
 | 
				
			|||||||
/** Manages the lifecycle of the window displaying layouts. */
 | 
					/** Manages the lifecycle of the window displaying layouts. */
 | 
				
			||||||
G_DECLARE_FINAL_TYPE (ServerContextService, server_context_service, SERVER, CONTEXT_SERVICE, GObject)
 | 
					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 vis_manager *visman);
 | 
					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);
 | 
				
			||||||
enum squeek_arrangement_kind server_context_service_get_layout_type(ServerContextService *);
 | 
					enum squeek_arrangement_kind server_context_service_get_layout_type(ServerContextService *);
 | 
				
			||||||
void server_context_service_force_show_keyboard (ServerContextService *self);
 | 
					void server_context_service_force_show_keyboard (ServerContextService *self);
 | 
				
			||||||
void server_context_service_hide_keyboard (ServerContextService *self);
 | 
					void server_context_service_hide_keyboard (ServerContextService *self);
 | 
				
			||||||
 | 
				
			|||||||
@ -25,7 +25,6 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#include "config.h"
 | 
					#include "config.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include "animation.h"
 | 
					 | 
				
			||||||
#include "eek/eek.h"
 | 
					#include "eek/eek.h"
 | 
				
			||||||
#include "eekboard/eekboard-context-service.h"
 | 
					#include "eekboard/eekboard-context-service.h"
 | 
				
			||||||
#include "dbus.h"
 | 
					#include "dbus.h"
 | 
				
			||||||
@ -47,15 +46,18 @@ typedef enum _SqueekboardDebugFlags {
 | 
				
			|||||||
} SqueekboardDebugFlags;
 | 
					} SqueekboardDebugFlags;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Global application state
 | 
					/// Some state, some IO components, all mixed together.
 | 
				
			||||||
 | 
					/// Better move what's possible to state::Application,
 | 
				
			||||||
 | 
					/// or secondary data structures of the same general shape.
 | 
				
			||||||
struct squeekboard {
 | 
					struct squeekboard {
 | 
				
			||||||
    struct squeek_wayland wayland; // Just hooks.
 | 
					    struct squeek_wayland wayland; // Just hooks.
 | 
				
			||||||
    DBusHandler *dbus_handler; // Controls visibility of the OSK.
 | 
					    DBusHandler *dbus_handler; // Controls visibility of the OSK.
 | 
				
			||||||
    EekboardContextService *settings_context; // Gsettings hooks.
 | 
					    EekboardContextService *settings_context; // Gsettings hooks.
 | 
				
			||||||
    ServerContextService *ui_context; // mess, includes the entire UI
 | 
					    ServerContextService *ui_context; // mess, includes the entire UI
 | 
				
			||||||
    struct submission *submission; // Wayland text input handling.
 | 
					    /// Currently wanted layout. TODO: merge into state::Application
 | 
				
			||||||
    struct squeek_layout_state layout_choice; // Currently wanted layout.
 | 
					    struct squeek_layout_state layout_choice;
 | 
				
			||||||
    struct ui_manager *ui_manager; // UI shape tracker/chooser. TODO: merge with layuot choice
 | 
					    /// UI shape tracker/chooser. TODO: merge into state::Application
 | 
				
			||||||
 | 
					    struct ui_manager *ui_manager;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -282,6 +284,21 @@ phosh_theme_init (void)
 | 
				
			|||||||
    g_object_set (G_OBJECT (gtk_settings), "gtk-application-prefer-dark-theme", TRUE, NULL);
 | 
					    g_object_set (G_OBJECT (gtk_settings), "gtk-application-prefer-dark-theme", TRUE, NULL);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Create Rust objects in one go,
 | 
				
			||||||
 | 
					/// to avoid crossing the language barrier and losing type information
 | 
				
			||||||
 | 
					static struct rsobjects create_rsobjects(struct zwp_input_method_manager_v2 *immanager,
 | 
				
			||||||
 | 
					                                         struct zwp_virtual_keyboard_manager_v1 *vkmanager,
 | 
				
			||||||
 | 
					                                         struct wl_seat *seat) {
 | 
				
			||||||
 | 
					    struct zwp_input_method_v2 *im = NULL;
 | 
				
			||||||
 | 
					    if (immanager) {
 | 
				
			||||||
 | 
					        im = zwp_input_method_manager_v2_get_input_method(immanager, seat);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    struct zwp_virtual_keyboard_v1 *vk = NULL;
 | 
				
			||||||
 | 
					    if (vkmanager) {
 | 
				
			||||||
 | 
					        vk = zwp_virtual_keyboard_manager_v1_create_virtual_keyboard(vkmanager, seat);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return squeek_rsobjects_new(im, vk);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static GDebugKey debug_keys[] =
 | 
					static GDebugKey debug_keys[] =
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@ -377,10 +394,9 @@ main (int argc, char **argv)
 | 
				
			|||||||
        g_warning("Wayland input method interface not available");
 | 
					        g_warning("Wayland input method interface not available");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    struct rsobjects rsobjects = create_rsobjects(instance.wayland.input_method_manager,
 | 
				
			||||||
    struct channel ui_channel = main_loop_channel_new();
 | 
					        instance.wayland.virtual_keyboard_manager,
 | 
				
			||||||
 | 
					        instance.wayland.seat);
 | 
				
			||||||
    struct squeek_animation_visibility_manager *animman = squeek_animation_visibility_manager_new(ui_channel.sender);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    instance.ui_manager = squeek_uiman_new();
 | 
					    instance.ui_manager = squeek_uiman_new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -401,7 +417,7 @@ main (int argc, char **argv)
 | 
				
			|||||||
    guint owner_id = 0;
 | 
					    guint owner_id = 0;
 | 
				
			||||||
    DBusHandler *service = NULL;
 | 
					    DBusHandler *service = NULL;
 | 
				
			||||||
    if (connection) {
 | 
					    if (connection) {
 | 
				
			||||||
        service = dbus_handler_new(connection, DBUS_SERVICE_PATH, animman);
 | 
					        service = dbus_handler_new(connection, DBUS_SERVICE_PATH, rsobjects.state_manager);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (service == NULL) {
 | 
					        if (service == NULL) {
 | 
				
			||||||
            g_printerr ("Can't create dbus server\n");
 | 
					            g_printerr ("Can't create dbus server\n");
 | 
				
			||||||
@ -422,38 +438,30 @@ main (int argc, char **argv)
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    struct vis_manager *vis_manager = squeek_visman_new(animman);
 | 
					    eekboard_context_service_set_submission(instance.settings_context, rsobjects.submission);
 | 
				
			||||||
 | 
					 | 
				
			||||||
    instance.submission = get_submission(instance.wayland.input_method_manager,
 | 
					 | 
				
			||||||
                                         instance.wayland.virtual_keyboard_manager,
 | 
					 | 
				
			||||||
                                         vis_manager,
 | 
					 | 
				
			||||||
                                         instance.wayland.seat,
 | 
					 | 
				
			||||||
                                         instance.settings_context);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    eekboard_context_service_set_submission(instance.settings_context, instance.submission);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ServerContextService *ui_context = server_context_service_new(
 | 
					    ServerContextService *ui_context = server_context_service_new(
 | 
				
			||||||
                instance.settings_context,
 | 
					                instance.settings_context,
 | 
				
			||||||
                instance.submission,
 | 
					                rsobjects.submission,
 | 
				
			||||||
                &instance.layout_choice,
 | 
					                &instance.layout_choice,
 | 
				
			||||||
                instance.ui_manager,
 | 
					                instance.ui_manager,
 | 
				
			||||||
                vis_manager);
 | 
					                rsobjects.state_manager);
 | 
				
			||||||
    if (!ui_context) {
 | 
					    if (!ui_context) {
 | 
				
			||||||
        g_error("Could not initialize GUI");
 | 
					        g_error("Could not initialize GUI");
 | 
				
			||||||
        exit(1);
 | 
					        exit(1);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    instance.ui_context = ui_context;
 | 
					    instance.ui_context = ui_context;
 | 
				
			||||||
    register_ui_loop_handler(ui_channel.receiver, instance.ui_context, instance.dbus_handler);
 | 
					    register_ui_loop_handler(rsobjects.receiver, instance.ui_context, instance.dbus_handler);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    session_register();
 | 
					    session_register();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (debug_flags & SQUEEKBOARD_DEBUG_FLAG_FORCE_SHOW) {
 | 
					 | 
				
			||||||
        server_context_service_force_show_keyboard (ui_context);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (debug_flags & SQUEEKBOARD_DEBUG_FLAG_GTK_INSPECTOR) {
 | 
					    if (debug_flags & SQUEEKBOARD_DEBUG_FLAG_GTK_INSPECTOR) {
 | 
				
			||||||
        gtk_window_set_interactive_debugging (TRUE);
 | 
					        gtk_window_set_interactive_debugging (TRUE);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (debug_flags & SQUEEKBOARD_DEBUG_FLAG_FORCE_SHOW) {
 | 
				
			||||||
 | 
					        squeek_state_send_force_visible (rsobjects.state_manager);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    loop = g_main_loop_new (NULL, FALSE);
 | 
					    loop = g_main_loop_new (NULL, FALSE);
 | 
				
			||||||
    g_main_loop_run (loop);
 | 
					    g_main_loop_run (loop);
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										417
									
								
								src/state.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										417
									
								
								src/state.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,417 @@
 | 
				
			|||||||
 | 
					/* Copyright (C) 2021 Purism SPC
 | 
				
			||||||
 | 
					 * SPDX-License-Identifier: GPL-3.0+
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/*! Application-wide state is stored here.
 | 
				
			||||||
 | 
					 * It's driven by the loop defined in the loop module. */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::animation;
 | 
				
			||||||
 | 
					use crate::imservice::{ ContentHint, ContentPurpose };
 | 
				
			||||||
 | 
					use crate::main::{ Commands, PanelCommand };
 | 
				
			||||||
 | 
					use std::time::Instant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone, Copy)]
 | 
				
			||||||
 | 
					pub enum Presence {
 | 
				
			||||||
 | 
					    Present,
 | 
				
			||||||
 | 
					    Missing,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone)]
 | 
				
			||||||
 | 
					pub struct InputMethodDetails {
 | 
				
			||||||
 | 
					    pub hint: ContentHint,
 | 
				
			||||||
 | 
					    pub purpose: ContentPurpose,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone)]
 | 
				
			||||||
 | 
					pub enum InputMethod {
 | 
				
			||||||
 | 
					    Active(InputMethodDetails),
 | 
				
			||||||
 | 
					    InactiveSince(Instant),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Incoming events
 | 
				
			||||||
 | 
					#[derive(Clone)]
 | 
				
			||||||
 | 
					pub enum Event {
 | 
				
			||||||
 | 
					    InputMethod(InputMethod),
 | 
				
			||||||
 | 
					    Visibility(visibility::Event),
 | 
				
			||||||
 | 
					    PhysicalKeyboard(Presence),
 | 
				
			||||||
 | 
					    /// Event triggered because a moment in time passed.
 | 
				
			||||||
 | 
					    /// Use to animate state transitions.
 | 
				
			||||||
 | 
					    /// The value is the ideal arrival time.
 | 
				
			||||||
 | 
					    TimeoutReached(Instant),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<InputMethod> for Event {
 | 
				
			||||||
 | 
					    fn from(im: InputMethod) -> Self {
 | 
				
			||||||
 | 
					        Self::InputMethod(im)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod visibility {
 | 
				
			||||||
 | 
					    #[derive(Clone)]
 | 
				
			||||||
 | 
					    pub enum Event {
 | 
				
			||||||
 | 
					        /// User requested the panel to show
 | 
				
			||||||
 | 
					        ForceVisible,
 | 
				
			||||||
 | 
					        /// The user requested the panel to go down
 | 
				
			||||||
 | 
					        ForceHidden,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[derive(Clone, PartialEq, Debug, Copy)]
 | 
				
			||||||
 | 
					    pub enum State {
 | 
				
			||||||
 | 
					        /// Last interaction was user forcing the panel to go visible
 | 
				
			||||||
 | 
					        ForcedVisible,
 | 
				
			||||||
 | 
					        /// Last interaction was user forcing the panel to hide
 | 
				
			||||||
 | 
					        ForcedHidden,
 | 
				
			||||||
 | 
					        /// Last interaction was the input method changing active state
 | 
				
			||||||
 | 
					        NotForced,
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// The outwardly visible state.
 | 
				
			||||||
 | 
					#[derive(Clone)]
 | 
				
			||||||
 | 
					pub struct Outcome {
 | 
				
			||||||
 | 
					    pub visibility: animation::Outcome,
 | 
				
			||||||
 | 
					    pub im: InputMethod,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Outcome {
 | 
				
			||||||
 | 
					    /// Returns the commands needed to apply changes as required by the new state.
 | 
				
			||||||
 | 
					    /// This implementation doesn't actually take the old state into account,
 | 
				
			||||||
 | 
					    /// instead issuing all the commands as needed to reach the new state.
 | 
				
			||||||
 | 
					    /// The receivers of the commands bear the burden
 | 
				
			||||||
 | 
					    /// of checking if the commands end up being no-ops.
 | 
				
			||||||
 | 
					    pub fn get_commands_to_reach(&self, new_state: &Self) -> Commands {
 | 
				
			||||||
 | 
					        let layout_hint_set = match new_state {
 | 
				
			||||||
 | 
					            Outcome {
 | 
				
			||||||
 | 
					                visibility: animation::Outcome::Visible,
 | 
				
			||||||
 | 
					                im: InputMethod::Active(hints),
 | 
				
			||||||
 | 
					            } => Some(hints.clone()),
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            Outcome {
 | 
				
			||||||
 | 
					                visibility: animation::Outcome::Visible,
 | 
				
			||||||
 | 
					                im: InputMethod::InactiveSince(_),
 | 
				
			||||||
 | 
					            } => Some(InputMethodDetails {
 | 
				
			||||||
 | 
					                hint: ContentHint::NONE,
 | 
				
			||||||
 | 
					                purpose: ContentPurpose::Normal,
 | 
				
			||||||
 | 
					            }),
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            Outcome {
 | 
				
			||||||
 | 
					                visibility: animation::Outcome::Hidden,
 | 
				
			||||||
 | 
					                ..
 | 
				
			||||||
 | 
					            } => None,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let (dbus_visible_set, panel_visibility) = match new_state.visibility {
 | 
				
			||||||
 | 
					            animation::Outcome::Visible => (Some(true), Some(PanelCommand::Show)),
 | 
				
			||||||
 | 
					            animation::Outcome::Hidden => (Some(false), Some(PanelCommand::Hide)),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Commands {
 | 
				
			||||||
 | 
					            panel_visibility,
 | 
				
			||||||
 | 
					            layout_hint_set,
 | 
				
			||||||
 | 
					            dbus_visible_set,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// The actual logic of the program.
 | 
				
			||||||
 | 
					/// At this moment, limited to calculating visibility and IM hints.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// It keeps the panel visible for a short time period after each hide request.
 | 
				
			||||||
 | 
					/// This prevents flickering on quick successive enable/disable events.
 | 
				
			||||||
 | 
					/// It does not treat user-driven hiding in a special way.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// This is the "functional core".
 | 
				
			||||||
 | 
					/// All state changes return the next state and the optimal time for the next check.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// This state tracker can be driven by any event loop.
 | 
				
			||||||
 | 
					#[derive(Clone)]
 | 
				
			||||||
 | 
					pub struct Application {
 | 
				
			||||||
 | 
					    pub im: InputMethod,
 | 
				
			||||||
 | 
					    pub visibility_override: visibility::State,
 | 
				
			||||||
 | 
					    pub physical_keyboard: Presence,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Application {
 | 
				
			||||||
 | 
					    /// A conservative default, ignoring the actual state of things.
 | 
				
			||||||
 | 
					    /// It will initially show the keyboard for a blink.
 | 
				
			||||||
 | 
					    // The ignorance might actually be desired,
 | 
				
			||||||
 | 
					    // as it allows for startup without waiting for a system check.
 | 
				
			||||||
 | 
					    // The downside is that adding actual state should not cause transitions.
 | 
				
			||||||
 | 
					    // Another acceptable alternative is to allow explicitly uninitialized parts.
 | 
				
			||||||
 | 
					    pub fn new(now: Instant) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            im: InputMethod::InactiveSince(now),
 | 
				
			||||||
 | 
					            visibility_override: visibility::State::NotForced,
 | 
				
			||||||
 | 
					            physical_keyboard: Presence::Missing,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn apply_event(self, event: Event, _now: Instant) -> Self {
 | 
				
			||||||
 | 
					        match event {
 | 
				
			||||||
 | 
					            Event::TimeoutReached(_) => self,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Event::Visibility(visibility) => Self {
 | 
				
			||||||
 | 
					                visibility_override: match visibility {
 | 
				
			||||||
 | 
					                    visibility::Event::ForceHidden => visibility::State::ForcedHidden,
 | 
				
			||||||
 | 
					                    visibility::Event::ForceVisible => visibility::State::ForcedVisible,
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                ..self
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Event::PhysicalKeyboard(presence) => Self {
 | 
				
			||||||
 | 
					                physical_keyboard: presence,
 | 
				
			||||||
 | 
					                ..self
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Event::InputMethod(new_im) => match (self.im.clone(), new_im) {
 | 
				
			||||||
 | 
					                (InputMethod::Active(_old), InputMethod::Active(new_im))
 | 
				
			||||||
 | 
					                => Self {
 | 
				
			||||||
 | 
					                    im: InputMethod::Active(new_im),
 | 
				
			||||||
 | 
					                    ..self
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                // For changes in active state, remove user's visibility override.
 | 
				
			||||||
 | 
					                // Both cases spelled out explicitly, rather than by the wildcard,
 | 
				
			||||||
 | 
					                // to not lose the notion that it's the opposition that matters
 | 
				
			||||||
 | 
					                (InputMethod::InactiveSince(_old), InputMethod::Active(new_im))
 | 
				
			||||||
 | 
					                => Self {
 | 
				
			||||||
 | 
					                    im: InputMethod::Active(new_im),
 | 
				
			||||||
 | 
					                    visibility_override: visibility::State::NotForced,
 | 
				
			||||||
 | 
					                    ..self
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                (InputMethod::Active(_old), InputMethod::InactiveSince(since))
 | 
				
			||||||
 | 
					                => Self {
 | 
				
			||||||
 | 
					                    im: InputMethod::InactiveSince(since),
 | 
				
			||||||
 | 
					                    visibility_override: visibility::State::NotForced,
 | 
				
			||||||
 | 
					                    ..self
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                // This is a weird case, there's no need to update an inactive state.
 | 
				
			||||||
 | 
					                // But it's not wrong, just superfluous.
 | 
				
			||||||
 | 
					                (InputMethod::InactiveSince(old), InputMethod::InactiveSince(_new))
 | 
				
			||||||
 | 
					                => Self {
 | 
				
			||||||
 | 
					                    // New is going to be newer than old, so it can be ignored.
 | 
				
			||||||
 | 
					                    // It was already inactive at that moment.
 | 
				
			||||||
 | 
					                    im: InputMethod::InactiveSince(old),
 | 
				
			||||||
 | 
					                    ..self
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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 }
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            im: self.im.clone(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Returns the next time to update the outcome.
 | 
				
			||||||
 | 
					    pub fn get_next_wake(&self, now: Instant) -> Option<Instant> {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            Self {
 | 
				
			||||||
 | 
					                visibility_override: visibility::State::NotForced,
 | 
				
			||||||
 | 
					                im: InputMethod::InactiveSince(since),
 | 
				
			||||||
 | 
					                ..
 | 
				
			||||||
 | 
					            } => {
 | 
				
			||||||
 | 
					                let anim_end = *since + animation::HIDING_TIMEOUT;
 | 
				
			||||||
 | 
					                if now < anim_end { Some(anim_end) }
 | 
				
			||||||
 | 
					                else { None }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            _ => None,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(test)]
 | 
				
			||||||
 | 
					mod test {
 | 
				
			||||||
 | 
					    use super::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    use std::time::Duration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn imdetails_new() -> InputMethodDetails {
 | 
				
			||||||
 | 
					        InputMethodDetails {
 | 
				
			||||||
 | 
					            purpose: ContentPurpose::Normal,
 | 
				
			||||||
 | 
					            hint: ContentHint::NONE,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Test the original delay scenario: no flicker on quick switches.
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn avoid_hide() {
 | 
				
			||||||
 | 
					        let start = Instant::now(); // doesn't matter when. It would be better to have a reproducible value though
 | 
				
			||||||
 | 
					        let mut now = start;
 | 
				
			||||||
 | 
					        let state = Application {
 | 
				
			||||||
 | 
					            im: InputMethod::Active(imdetails_new()),
 | 
				
			||||||
 | 
					            physical_keyboard: Presence::Missing,
 | 
				
			||||||
 | 
					            visibility_override: visibility::State::NotForced,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        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!(
 | 
				
			||||||
 | 
					                state.get_outcome(now).visibility,
 | 
				
			||||||
 | 
					                animation::Outcome::Visible,
 | 
				
			||||||
 | 
					                "Hidden when it should remain visible: {:?}",
 | 
				
			||||||
 | 
					                now.saturating_duration_since(start),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let state = state.apply_event(Event::InputMethod(InputMethod::Active(imdetails_new())), now);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert_eq!(state.get_outcome(now).visibility, animation::Outcome::Visible);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Make sure that hiding works when input method goes away
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn hide() {
 | 
				
			||||||
 | 
					        let start = Instant::now(); // doesn't matter when. It would be better to have a reproducible value though
 | 
				
			||||||
 | 
					        let mut now = start;
 | 
				
			||||||
 | 
					        let state = Application {
 | 
				
			||||||
 | 
					            im: InputMethod::Active(imdetails_new()),
 | 
				
			||||||
 | 
					            physical_keyboard: Presence::Missing,
 | 
				
			||||||
 | 
					            visibility_override: visibility::State::NotForced,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        while let animation::Outcome::Visible = state.get_outcome(now).visibility {
 | 
				
			||||||
 | 
					            now += Duration::from_millis(1);
 | 
				
			||||||
 | 
					            assert!(
 | 
				
			||||||
 | 
					                now < start + Duration::from_millis(250),
 | 
				
			||||||
 | 
					                "Hiding too slow: {:?}",
 | 
				
			||||||
 | 
					                now.saturating_duration_since(start),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    /// Check against the false showing bug.
 | 
				
			||||||
 | 
					    /// Expectation: it will get hidden and not appear again
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn false_show() {
 | 
				
			||||||
 | 
					        let start = Instant::now(); // doesn't matter when. It would be better to have a reproducible value though
 | 
				
			||||||
 | 
					        let mut now = start;
 | 
				
			||||||
 | 
					        let state = Application {
 | 
				
			||||||
 | 
					            im: InputMethod::Active(imdetails_new()),
 | 
				
			||||||
 | 
					            physical_keyboard: Presence::Missing,
 | 
				
			||||||
 | 
					            visibility_override: visibility::State::NotForced,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        // This reflects the sequence from Wayland:
 | 
				
			||||||
 | 
					        // disable, disable, enable, disable
 | 
				
			||||||
 | 
					        // all in a single batch.
 | 
				
			||||||
 | 
					        let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);
 | 
				
			||||||
 | 
					        let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);
 | 
				
			||||||
 | 
					        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 {
 | 
				
			||||||
 | 
					            now += Duration::from_millis(1);
 | 
				
			||||||
 | 
					            assert!(
 | 
				
			||||||
 | 
					                now < start + Duration::from_millis(250),
 | 
				
			||||||
 | 
					                "Still not hidden: {:?}",
 | 
				
			||||||
 | 
					                now.saturating_duration_since(start),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // One second without appearing again
 | 
				
			||||||
 | 
					        for _i in 0..1000 {
 | 
				
			||||||
 | 
					            now += Duration::from_millis(1);
 | 
				
			||||||
 | 
					            assert_eq!(
 | 
				
			||||||
 | 
					                state.get_outcome(now).visibility,
 | 
				
			||||||
 | 
					                animation::Outcome::Hidden,
 | 
				
			||||||
 | 
					                "Appeared unnecessarily: {:?}",
 | 
				
			||||||
 | 
					                now.saturating_duration_since(start),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn force_visible() {
 | 
				
			||||||
 | 
					        let start = Instant::now(); // doesn't matter when. It would be better to have a reproducible value though
 | 
				
			||||||
 | 
					        let mut now = start;
 | 
				
			||||||
 | 
					        let state = Application {
 | 
				
			||||||
 | 
					            im: InputMethod::InactiveSince(now),
 | 
				
			||||||
 | 
					            physical_keyboard: Presence::Missing,
 | 
				
			||||||
 | 
					            visibility_override: visibility::State::NotForced,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        now += Duration::from_secs(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let state = state.apply_event(Event::Visibility(visibility::Event::ForceVisible), now);
 | 
				
			||||||
 | 
					        assert_eq!(
 | 
				
			||||||
 | 
					            state.get_outcome(now).visibility,
 | 
				
			||||||
 | 
					            animation::Outcome::Visible,
 | 
				
			||||||
 | 
					            "Failed to show: {:?}",
 | 
				
			||||||
 | 
					            now.saturating_duration_since(start),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        now += Duration::from_secs(1);
 | 
				
			||||||
 | 
					        let state = state.apply_event(Event::InputMethod(InputMethod::Active(imdetails_new())), now);
 | 
				
			||||||
 | 
					        now += Duration::from_secs(1);
 | 
				
			||||||
 | 
					        let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);
 | 
				
			||||||
 | 
					        now += Duration::from_secs(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert_eq!(
 | 
				
			||||||
 | 
					            state.get_outcome(now).visibility,
 | 
				
			||||||
 | 
					            animation::Outcome::Hidden,
 | 
				
			||||||
 | 
					            "Failed to release forced visibility: {:?}",
 | 
				
			||||||
 | 
					            now.saturating_duration_since(start),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[test]
 | 
				
			||||||
 | 
					    fn keyboard_present() {
 | 
				
			||||||
 | 
					        let start = Instant::now(); // doesn't matter when. It would be better to have a reproducible value though
 | 
				
			||||||
 | 
					        let mut now = start;
 | 
				
			||||||
 | 
					        let state = Application {
 | 
				
			||||||
 | 
					            im: InputMethod::Active(imdetails_new()),
 | 
				
			||||||
 | 
					            physical_keyboard: Presence::Missing,
 | 
				
			||||||
 | 
					            visibility_override: visibility::State::NotForced,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        now += Duration::from_secs(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let state = state.apply_event(Event::PhysicalKeyboard(Presence::Present), now);
 | 
				
			||||||
 | 
					        assert_eq!(
 | 
				
			||||||
 | 
					            state.get_outcome(now).visibility,
 | 
				
			||||||
 | 
					            animation::Outcome::Hidden,
 | 
				
			||||||
 | 
					            "Failed to hide: {:?}",
 | 
				
			||||||
 | 
					            now.saturating_duration_since(start),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        now += Duration::from_secs(1);
 | 
				
			||||||
 | 
					        let state = state.apply_event(Event::InputMethod(InputMethod::InactiveSince(now)), now);
 | 
				
			||||||
 | 
					        now += Duration::from_secs(1);
 | 
				
			||||||
 | 
					        let state = state.apply_event(Event::InputMethod(InputMethod::Active(imdetails_new())), now);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert_eq!(
 | 
				
			||||||
 | 
					            state.get_outcome(now).visibility,
 | 
				
			||||||
 | 
					            animation::Outcome::Hidden,
 | 
				
			||||||
 | 
					            "Failed to remain hidden: {:?}",
 | 
				
			||||||
 | 
					            now.saturating_duration_since(start),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        now += Duration::from_secs(1);
 | 
				
			||||||
 | 
					        let state = state.apply_event(Event::PhysicalKeyboard(Presence::Missing), now);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        assert_eq!(
 | 
				
			||||||
 | 
					            state.get_outcome(now).visibility,
 | 
				
			||||||
 | 
					            animation::Outcome::Visible,
 | 
				
			||||||
 | 
					            "Failed to appear: {:?}",
 | 
				
			||||||
 | 
					            now.saturating_duration_since(start),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -4,20 +4,12 @@
 | 
				
			|||||||
#include "input-method-unstable-v2-client-protocol.h"
 | 
					#include "input-method-unstable-v2-client-protocol.h"
 | 
				
			||||||
#include "virtual-keyboard-unstable-v1-client-protocol.h"
 | 
					#include "virtual-keyboard-unstable-v1-client-protocol.h"
 | 
				
			||||||
#include "eek/eek-types.h"
 | 
					#include "eek/eek-types.h"
 | 
				
			||||||
 | 
					#include "main.h"
 | 
				
			||||||
#include "src/ui_manager.h"
 | 
					#include "src/ui_manager.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
struct submission;
 | 
					 | 
				
			||||||
struct squeek_layout;
 | 
					struct squeek_layout;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
struct submission* get_submission(struct zwp_input_method_manager_v2 *immanager,
 | 
					 | 
				
			||||||
                                  struct zwp_virtual_keyboard_manager_v1 *vkmanager,
 | 
					 | 
				
			||||||
                                  struct vis_manager *vis_manager,
 | 
					 | 
				
			||||||
                                  struct wl_seat *seat,
 | 
					 | 
				
			||||||
                                  EekboardContextService *state);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Defined in Rust
 | 
					// Defined in Rust
 | 
				
			||||||
struct submission* submission_new(struct zwp_input_method_v2 *im, struct zwp_virtual_keyboard_v1 *vk, EekboardContextService *state, struct vis_manager *vis_manager);
 | 
					 | 
				
			||||||
uint8_t submission_hint_available(struct submission *self);
 | 
					uint8_t submission_hint_available(struct submission *self);
 | 
				
			||||||
void submission_set_ui(struct submission *self, ServerContextService *ui_context);
 | 
					 | 
				
			||||||
void submission_use_layout(struct submission *self, struct squeek_layout *layout, uint32_t time);
 | 
					void submission_use_layout(struct submission *self, struct squeek_layout *layout, uint32_t time);
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
 | 
				
			|||||||
@ -19,12 +19,13 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use std::collections::HashSet;
 | 
					use std::collections::HashSet;
 | 
				
			||||||
use std::ffi::CString;
 | 
					use std::ffi::CString;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::vkeyboard::c::ZwpVirtualKeyboardV1;
 | 
				
			||||||
use ::action::Modifier;
 | 
					use ::action::Modifier;
 | 
				
			||||||
use ::imservice;
 | 
					use ::imservice;
 | 
				
			||||||
use ::imservice::IMService;
 | 
					use ::imservice::IMService;
 | 
				
			||||||
use ::keyboard::{ KeyCode, KeyStateId, Modifiers, PressType };
 | 
					use ::keyboard::{ KeyCode, KeyStateId, Modifiers, PressType };
 | 
				
			||||||
use ::layout;
 | 
					use ::layout;
 | 
				
			||||||
use ::ui_manager::VisibilityManager;
 | 
					 | 
				
			||||||
use ::util::vec_remove;
 | 
					use ::util::vec_remove;
 | 
				
			||||||
use ::vkeyboard;
 | 
					use ::vkeyboard;
 | 
				
			||||||
use ::vkeyboard::VirtualKeyboard;
 | 
					use ::vkeyboard::VirtualKeyboard;
 | 
				
			||||||
@ -35,51 +36,11 @@ use std::iter::FromIterator;
 | 
				
			|||||||
/// Gathers stuff defined in C or called by C
 | 
					/// Gathers stuff defined in C or called by C
 | 
				
			||||||
pub mod c {
 | 
					pub mod c {
 | 
				
			||||||
    use super::*;
 | 
					    use super::*;
 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    use std::os::raw::c_void;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    use ::imservice::c::InputMethod;
 | 
					    use crate::util::c::Wrapped;
 | 
				
			||||||
    use ::util::c::Wrapped;
 | 
					 | 
				
			||||||
    use ::vkeyboard::c::ZwpVirtualKeyboardV1;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // The following defined in C
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// EekboardContextService*
 | 
					 | 
				
			||||||
    #[repr(transparent)]
 | 
					 | 
				
			||||||
    pub struct StateManager(*const c_void);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub type Submission = Wrapped<super::Submission>;
 | 
					    pub type Submission = Wrapped<super::Submission>;
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    #[no_mangle]
 | 
					 | 
				
			||||||
    pub extern "C"
 | 
					 | 
				
			||||||
    fn submission_new(
 | 
					 | 
				
			||||||
        im: *mut InputMethod,
 | 
					 | 
				
			||||||
        vk: ZwpVirtualKeyboardV1,
 | 
					 | 
				
			||||||
        state_manager: *const StateManager,
 | 
					 | 
				
			||||||
        visibility_manager: Wrapped<VisibilityManager>,
 | 
					 | 
				
			||||||
    ) -> Submission {
 | 
					 | 
				
			||||||
        let imservice = if im.is_null() {
 | 
					 | 
				
			||||||
            None
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            let visibility_manager = visibility_manager.clone_ref();
 | 
					 | 
				
			||||||
            Some(IMService::new(
 | 
					 | 
				
			||||||
                im,
 | 
					 | 
				
			||||||
                state_manager,
 | 
					 | 
				
			||||||
                Box::new(move |active| visibility_manager.borrow_mut().set_im_active(active)),
 | 
					 | 
				
			||||||
            ))
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
        // TODO: add vkeyboard too
 | 
					 | 
				
			||||||
        Wrapped::new(
 | 
					 | 
				
			||||||
            super::Submission {
 | 
					 | 
				
			||||||
                imservice,
 | 
					 | 
				
			||||||
                modifiers_active: Vec::new(),
 | 
					 | 
				
			||||||
                virtual_keyboard: VirtualKeyboard(vk),
 | 
					 | 
				
			||||||
                pressed: Vec::new(),
 | 
					 | 
				
			||||||
                keymap_fds: Vec::new(),
 | 
					 | 
				
			||||||
                keymap_idx: None,
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    #[no_mangle]
 | 
					    #[no_mangle]
 | 
				
			||||||
    pub extern "C"
 | 
					    pub extern "C"
 | 
				
			||||||
@ -131,6 +92,17 @@ pub enum SubmitData<'a> {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl Submission {
 | 
					impl Submission {
 | 
				
			||||||
 | 
					    pub fn new(vk: ZwpVirtualKeyboardV1, imservice: Option<Box<IMService>>) -> Self {
 | 
				
			||||||
 | 
					        Submission {
 | 
				
			||||||
 | 
					            imservice,
 | 
				
			||||||
 | 
					            modifiers_active: Vec::new(),
 | 
				
			||||||
 | 
					            virtual_keyboard: VirtualKeyboard(vk),
 | 
				
			||||||
 | 
					            pressed: Vec::new(),
 | 
				
			||||||
 | 
					            keymap_fds: Vec::new(),
 | 
				
			||||||
 | 
					            keymap_idx: None,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// Sends a submit text event if possible;
 | 
					    /// Sends a submit text event if possible;
 | 
				
			||||||
    /// otherwise sends key press and makes a note of it
 | 
					    /// otherwise sends key press and makes a note of it
 | 
				
			||||||
    pub fn handle_press(
 | 
					    pub fn handle_press(
 | 
				
			||||||
 | 
				
			|||||||
@ -3,9 +3,9 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#include <inttypes.h>
 | 
					#include <inttypes.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include "animation.h"
 | 
					 | 
				
			||||||
#include "eek/eek-types.h"
 | 
					#include "eek/eek-types.h"
 | 
				
			||||||
#include "outputs.h"
 | 
					#include "outputs.h"
 | 
				
			||||||
 | 
					#include "main.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
struct ui_manager;
 | 
					struct ui_manager;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -15,6 +15,5 @@ uint32_t squeek_uiman_get_perceptual_height(struct ui_manager *uiman);
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
struct vis_manager;
 | 
					struct vis_manager;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
struct vis_manager *squeek_visman_new(struct squeek_animation_visibility_manager *animman);
 | 
					struct vis_manager *squeek_visman_new(struct squeek_state_manager *state_manager);
 | 
				
			||||||
void squeek_visman_set_keyboard_present(struct vis_manager *visman, uint32_t keyboard_present);
 | 
					 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
 | 
				
			|||||||
@ -1,54 +1,20 @@
 | 
				
			|||||||
/* Copyright (C) 2020 Purism SPC
 | 
					/* Copyright (C) 2020, 2021 Purism SPC
 | 
				
			||||||
 * SPDX-License-Identifier: GPL-3.0+
 | 
					 * SPDX-License-Identifier: GPL-3.0+
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/*! Centrally manages the shape of the UI widgets, and the choice of layout.
 | 
					/*! Centrally manages the shape of the UI widgets, and the choice of layout.
 | 
				
			||||||
 * 
 | 
					 * 
 | 
				
			||||||
 * Coordinates this based on information collated from all possible sources.
 | 
					 * Coordinates this based on information collated from all possible sources.
 | 
				
			||||||
 * 
 | 
					 | 
				
			||||||
 * Somewhat obsoleted by the `animation` module
 | 
					 | 
				
			||||||
 * (except keyboard presence calculation),
 | 
					 | 
				
			||||||
 * and could be folded into that tracker loop as another piece of state.
 | 
					 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::animation::{
 | 
					 | 
				
			||||||
    ThreadLoopDriver as Receiver,
 | 
					 | 
				
			||||||
    Event as ReceiverMessage,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use crate::logging;
 | 
					 | 
				
			||||||
use std::cmp::min;
 | 
					use std::cmp::min;
 | 
				
			||||||
use ::outputs::c::OutputHandle;
 | 
					use ::outputs::c::OutputHandle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::logging::Warn;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub mod c {
 | 
					pub mod c {
 | 
				
			||||||
    use super::*;
 | 
					    use super::*;
 | 
				
			||||||
    use ::util::c::Wrapped;
 | 
					    use ::util::c::Wrapped;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    use ::util::CloneOwned;
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    #[no_mangle]
 | 
					 | 
				
			||||||
    pub extern "C"
 | 
					 | 
				
			||||||
    fn squeek_visman_new(receiver: Wrapped<Receiver>) -> Wrapped<VisibilityManager> {
 | 
					 | 
				
			||||||
        Wrapped::new(VisibilityManager {
 | 
					 | 
				
			||||||
            receiver: receiver.clone_owned(),
 | 
					 | 
				
			||||||
            visibility_state: VisibilityFactors {
 | 
					 | 
				
			||||||
                im_active: false,
 | 
					 | 
				
			||||||
                physical_keyboard_present: false,
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    #[no_mangle]
 | 
					 | 
				
			||||||
    pub extern "C"
 | 
					 | 
				
			||||||
    fn squeek_visman_set_keyboard_present(visman: Wrapped<VisibilityManager>, present: u32) {
 | 
					 | 
				
			||||||
        let visman = visman.clone_ref();
 | 
					 | 
				
			||||||
        let mut visman = visman.borrow_mut();
 | 
					 | 
				
			||||||
        visman.set_keyboard_present(present != 0)
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    #[no_mangle]
 | 
					    #[no_mangle]
 | 
				
			||||||
    pub extern "C"
 | 
					    pub extern "C"
 | 
				
			||||||
    fn squeek_uiman_new() -> Wrapped<Manager> {
 | 
					    fn squeek_uiman_new() -> Wrapped<Manager> {
 | 
				
			||||||
@ -114,106 +80,3 @@ impl Manager {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(PartialEq, Debug)]
 | 
					 | 
				
			||||||
enum Visibility {
 | 
					 | 
				
			||||||
    Hidden,
 | 
					 | 
				
			||||||
    Visible,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Debug)]
 | 
					 | 
				
			||||||
enum VisibilityTransition {
 | 
					 | 
				
			||||||
    /// Hide immediately
 | 
					 | 
				
			||||||
    Hide,
 | 
					 | 
				
			||||||
    /// Hide if no show request comes soon
 | 
					 | 
				
			||||||
    Release,
 | 
					 | 
				
			||||||
    /// Show instantly
 | 
					 | 
				
			||||||
    Show,
 | 
					 | 
				
			||||||
    /// Don't do anything
 | 
					 | 
				
			||||||
    NoTransition,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Contains visibility policy
 | 
					 | 
				
			||||||
#[derive(Clone, Debug)]
 | 
					 | 
				
			||||||
struct VisibilityFactors {
 | 
					 | 
				
			||||||
    im_active: bool,
 | 
					 | 
				
			||||||
    physical_keyboard_present: bool,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl VisibilityFactors {
 | 
					 | 
				
			||||||
    /// Static policy.
 | 
					 | 
				
			||||||
    /// Use when transitioning from an undefined state (e.g. no UI before).
 | 
					 | 
				
			||||||
    fn desired(&self) -> Visibility {
 | 
					 | 
				
			||||||
        match self {
 | 
					 | 
				
			||||||
            VisibilityFactors {
 | 
					 | 
				
			||||||
                im_active: true,
 | 
					 | 
				
			||||||
                physical_keyboard_present: false,
 | 
					 | 
				
			||||||
            } => Visibility::Visible,
 | 
					 | 
				
			||||||
            _ => Visibility::Hidden,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    /// Stateful policy
 | 
					 | 
				
			||||||
    fn transition_to(&self, next: &Self) -> VisibilityTransition {
 | 
					 | 
				
			||||||
        use self::Visibility::*;
 | 
					 | 
				
			||||||
        let im_deactivation = self.im_active && !next.im_active;
 | 
					 | 
				
			||||||
        match (self.desired(), next.desired(), im_deactivation) {
 | 
					 | 
				
			||||||
            (Visible, Hidden, true) => VisibilityTransition::Release,
 | 
					 | 
				
			||||||
            (Visible, Hidden, _) => VisibilityTransition::Hide,
 | 
					 | 
				
			||||||
            (Hidden, Visible, _) => VisibilityTransition::Show,
 | 
					 | 
				
			||||||
            _ => VisibilityTransition::NoTransition,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
pub struct VisibilityManager {
 | 
					 | 
				
			||||||
    /// Forward changes there.
 | 
					 | 
				
			||||||
    receiver: Receiver,
 | 
					 | 
				
			||||||
    visibility_state: VisibilityFactors,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
impl VisibilityManager {
 | 
					 | 
				
			||||||
    fn apply_changes(&mut self, new: Self) {
 | 
					 | 
				
			||||||
        let request = match self.visibility_state.transition_to(&new.visibility_state) {
 | 
					 | 
				
			||||||
            VisibilityTransition::Hide => Some(ReceiverMessage::ForceHide),
 | 
					 | 
				
			||||||
            VisibilityTransition::Show => Some(ReceiverMessage::ClaimVisible),
 | 
					 | 
				
			||||||
            VisibilityTransition::Release => Some(ReceiverMessage::ReleaseVisible),
 | 
					 | 
				
			||||||
            VisibilityTransition::NoTransition => None,
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        if let Some(request) = request {
 | 
					 | 
				
			||||||
            new.receiver.send(request)
 | 
					 | 
				
			||||||
                .or_warn(&mut logging::Print, logging::Problem::Warning, "Can't send to animation manager");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        *self = new;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn set_im_active(&mut self, im_active: bool) {
 | 
					 | 
				
			||||||
        let new = VisibilityManager {
 | 
					 | 
				
			||||||
            visibility_state: VisibilityFactors {
 | 
					 | 
				
			||||||
                im_active,
 | 
					 | 
				
			||||||
                ..self.visibility_state.clone()
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            ..unsafe { self.clone() }
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
        self.apply_changes(new);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    pub fn set_keyboard_present(&mut self, keyboard_present: bool) {
 | 
					 | 
				
			||||||
        let new = VisibilityManager {
 | 
					 | 
				
			||||||
            visibility_state: VisibilityFactors {
 | 
					 | 
				
			||||||
                physical_keyboard_present: keyboard_present,
 | 
					 | 
				
			||||||
                ..self.visibility_state.clone()
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            ..unsafe { self.clone() }
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
        self.apply_changes(new);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    /// This is only a helper for getting desired visibility.
 | 
					 | 
				
			||||||
    unsafe fn clone(&self) -> Self {
 | 
					 | 
				
			||||||
        VisibilityManager {
 | 
					 | 
				
			||||||
            receiver: self.receiver.clone(),
 | 
					 | 
				
			||||||
            visibility_state: self.visibility_state.clone(),
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user