1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
/* 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;

use std::cmp;
use std::time::{ Duration, Instant };


/// Carries the incoming data to affect the actor state,
/// plus an event to help schedule timed events.
pub trait Event: Clone {
    fn new_timeout_reached(when: Instant) -> Self;
    /// Returns the value of the reached timeout, if this event carries the timeout.
    fn get_timeout_reached(&self) -> Option<Instant>;
}

/// The externally observable state of the actor.
pub trait Outcome {
    type Commands;
    
    /// Returns the instructions to emit in order to change the current visible state to the desired one.
    fn get_commands_to_reach(&self, desired: &Self) -> Self::Commands;
}

/// Contains and calculates the intenal state of the actor.
pub trait ActorState: Clone {
    type Event: Event;
    type Outcome: Outcome;
    /// Returns the new internal state after the event gets processed.
    fn apply_event(self, e: Self::Event, time: Instant) -> Self;
    /// Returns the observable state of the actor given this internal state.
    fn get_outcome(&self, time: Instant) -> Self::Outcome;
    /// Returns the next wake up to schedule if one is needed.
    /// This may be called at any time, so should always return the correct value.
    fn get_next_wake(&self, now: Instant) -> Option<Instant>;
}

/// This keeps the state of the tracker loop between iterations
#[derive(Clone)]
struct State<S> {
    state: S,
    scheduled_wakeup: Option<Instant>,
    last_update: Instant,
}

impl<S> State<S> {
    fn new(initial_state: S, 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<S: ActorState>(
    mut loop_state: State<S>,
    event: S::Event,
    now: Instant,
) -> (State<S>, <S::Outcome as Outcome>::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.get_timeout_reached() {
        Some(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
            }
        },
        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::panel;
    use crate::state;
    use crate::state::{ Application, InputMethod, InputMethodDetails, Presence, visibility };
    use crate::state::test::application_with_fake_output;

    fn imdetails_new() -> InputMethodDetails {
        InputMethodDetails {
            purpose: ContentPurpose::Normal,
            hint: ContentHint::NONE,
        }
    }

    // TODO: This should only test the scheduling in handle_event.
    // This means it should be separated from actual application logic,
    // and use a mock state instead.
    #[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,
            ..application_with_fake_output(start)
        };
        
        let l = State::new(state, now);
        let (l, commands) = handle_event(l, InputMethod::InactiveSince(now).into(), now);
        assert_matches!(commands.panel_visibility, Some(panel::Command::Show{..}));
        assert_eq!(l.scheduled_wakeup, Some(now + animation::HIDING_TIMEOUT));
        
        now += animation::HIDING_TIMEOUT;
        
        let (l, commands) = handle_event(l, state::Event::TimeoutReached(now), now);
        assert_eq!(commands.panel_visibility, Some(panel::Command::Hide));
        assert_eq!(l.scheduled_wakeup, None);
    }
}