feat(desktop): home app (system-only launch cap, LAUNCH_BADGE: apps+launch only), pm starts it with session (relaunch <=5); display keys F1/Alt+Tab next, F2 home, F4 close app (ROLE from launcher, terminal exempt), close returns to previous window; closed window -> app exits; shell 'name &'; launch by id; pm stack 8, storage 16 clients
This commit is contained in:
@@ -94,6 +94,16 @@ impl<F: Framebuffer, H: Host> DisplayServer<F, H> {
|
||||
&mut self.host
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn windows_front_for_test(&self) -> Option<usize> {
|
||||
self.windows.front()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn slot_for_test(&self, badge: usize) -> Option<usize> {
|
||||
self.windows.slot(badge)
|
||||
}
|
||||
|
||||
/// A window's size: the screen below the bar.
|
||||
fn window_size(&self) -> (Dimension, Dimension) {
|
||||
(self.fb.width(), Dimension::new(self.fb.height().raw().saturating_sub(BAR_HEIGHT)))
|
||||
@@ -193,6 +203,28 @@ impl<F: Framebuffer, H: Host> DisplayServer<F, H> {
|
||||
raster::draw_text(&mut self.fb, ScreenCoord::new(at), ScreenCoord::new(0), bar_fg, bar_bg, user);
|
||||
}
|
||||
|
||||
/// What a window key does.
|
||||
fn window_key(&mut self, key: usize) {
|
||||
let changed = match key {
|
||||
w::SWITCH_KEY => self.windows.cycle().is_some(),
|
||||
w::HOME_KEY => self.windows.with_role(w::role::HOME).is_some_and(|i| self.windows.raise(i)),
|
||||
_ => match self.windows.front().filter(|i| self.windows.role(*i) == Some(w::role::APP)) {
|
||||
Some(index) => {
|
||||
// Woken, it finds its window gone, and is done.
|
||||
if let Some(notify) = self.windows.notify(index) {
|
||||
self.host.signal(notify);
|
||||
}
|
||||
self.close(index);
|
||||
false
|
||||
}
|
||||
None => false,
|
||||
},
|
||||
};
|
||||
if changed {
|
||||
self.repaint();
|
||||
}
|
||||
}
|
||||
|
||||
/// Every window is told to ask for the theme again; the bar wears it.
|
||||
fn theme_changed(&mut self) {
|
||||
let mut told = [None; MAX_WINDOWS];
|
||||
@@ -220,6 +252,7 @@ impl<F: Framebuffer, H: Host> DisplayServer<F, H> {
|
||||
None => ([MALFORMED, 0, 0, 0], None),
|
||||
}
|
||||
}
|
||||
(w::ROLE, _) if matches!(words[1], w::role::APP | w::role::HOME) && self.windows.set_role(badge, words[1]) => ([w::OK, 0, 0, 0], None),
|
||||
(w::ACCENT, _) if words[1] <= 0xff_ffff && self.windows.set_accent(badge, words[1] as u32) => ([w::OK, 0, 0, 0], None),
|
||||
(w::PRESENT, _) => {
|
||||
let (x, y, width, height) = w::unpack_rect([words[1], words[2]]);
|
||||
@@ -248,9 +281,11 @@ impl<F: Framebuffer, H: Host> DisplayServer<F, H> {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_switch_key(event: [usize; 4]) -> bool {
|
||||
/// The display's own key in `event`, if it is one (pressed or let go).
|
||||
fn display_key(event: [usize; 4]) -> Option<usize> {
|
||||
use types::input::event::{FUNCTION_KEY, KEY_DOWN, KEY_UP};
|
||||
matches!(event[0], KEY_DOWN | KEY_UP) && event[1] == FUNCTION_KEY && event[2] == w::SWITCH_KEY
|
||||
let ours = matches!(event[0], KEY_DOWN | KEY_UP) && event[1] == FUNCTION_KEY;
|
||||
(ours && matches!(event[2], w::SWITCH_KEY | w::HOME_KEY | w::CLOSE_KEY)).then_some(event[2])
|
||||
}
|
||||
|
||||
impl<F: Framebuffer, H: Host> Server for DisplayServer<F, H> {
|
||||
@@ -306,14 +341,14 @@ impl<F: Framebuffer, H: Host> Server for DisplayServer<F, H> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The input server has events: the switch key is the display's, the
|
||||
/// rest go to the window in front.
|
||||
/// The input server has events: the window keys are the display's,
|
||||
/// the rest go to the window in front.
|
||||
fn signal(&mut self, _bits: usize) {
|
||||
let mut told = None;
|
||||
while let Some(event) = self.host.input() {
|
||||
if is_switch_key(event) {
|
||||
if event[0] == types::input::event::KEY_DOWN && self.windows.cycle().is_some() {
|
||||
self.repaint();
|
||||
if let Some(key) = display_key(event) {
|
||||
if event[0] == types::input::event::KEY_DOWN {
|
||||
self.window_key(key);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -259,3 +259,42 @@ fn only_the_launcher_sets_an_accent_and_it_is_in_the_theme() {
|
||||
assert_eq!(accent(&mut s, other), theme::Theme::WHIMSY_DARK.accent);
|
||||
assert_eq!(accent(&mut s, app), theme::with_accent(theme::Theme::WHIMSY_DARK, theme::Rgba::rgb(pink as u32)).accent);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_keys_raise_home_and_close_only_apps() {
|
||||
let key = |s: &mut Srv, k: usize| {
|
||||
s.host_mut_for_test().events = vec![[1, 10, k, 0], [2, 10, k, 0]];
|
||||
s.signal(0);
|
||||
};
|
||||
let mut s = server();
|
||||
let terminal = open(&mut s, 1, b"Terminal");
|
||||
let home = open(&mut s, 2, b"Home");
|
||||
let app = open(&mut s, 2, b"Tally");
|
||||
assert_eq!(s.handle_call(home, [w::ROLE, w::role::HOME, 0, 0], None).0[0], w::OK);
|
||||
assert_eq!(s.handle_call(app, [w::ROLE, w::role::APP, 0, 0], None).0[0], w::OK);
|
||||
assert_eq!(s.handle_call(app, [w::ROLE, w::role::HOME, 0, 0], None).0[0], MALFORMED, "once");
|
||||
s.handle_call(terminal, [w::BUFFER, 0, 0, 0], None);
|
||||
assert_eq!(s.handle_call(terminal, [w::ROLE, w::role::APP, 0, 0], None).0[0], MALFORMED, "not once it runs");
|
||||
let notify = CapSlot::new(9).unwrap();
|
||||
s.handle_call(app, [w::SUBSCRIBE, 0, 0, 0], Some(notify));
|
||||
|
||||
key(&mut s, w::HOME_KEY);
|
||||
assert_eq!(s.windows_front_for_test(), s.slot_for_test(home));
|
||||
key(&mut s, w::CLOSE_KEY);
|
||||
assert!(s.slot_for_test(home).is_some(), "Home is not closed");
|
||||
// The app in front: closed, and told.
|
||||
s.handle_call(app, [w::POLL, 0, 0, 0], None);
|
||||
key(&mut s, w::SWITCH_KEY);
|
||||
assert_eq!(s.windows_front_for_test(), s.slot_for_test(app));
|
||||
key(&mut s, w::CLOSE_KEY);
|
||||
assert_eq!(s.slot_for_test(app), None);
|
||||
assert!(s.host().signalled.contains(¬ify));
|
||||
assert_eq!(s.handle_call(app, [w::POLL, 0, 0, 0], None).0[0], MALFORMED, "its window is gone");
|
||||
// The terminal in front: kept.
|
||||
key(&mut s, w::SWITCH_KEY);
|
||||
while s.windows_front_for_test() != s.slot_for_test(terminal) {
|
||||
key(&mut s, w::SWITCH_KEY);
|
||||
}
|
||||
key(&mut s, w::CLOSE_KEY);
|
||||
assert!(s.slot_for_test(terminal).is_some());
|
||||
}
|
||||
|
||||
@@ -67,3 +67,19 @@ fn a_launchers_windows_are_found_by_its_badge() {
|
||||
let (b, _) = w.open(5, b"two").unwrap();
|
||||
assert_eq!(w.opened_by(5).collect::<Vec<_>>(), [a, b]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closing_the_front_window_brings_back_the_one_before_it() {
|
||||
let mut w: Windows<u8> = Windows::new();
|
||||
let (t, _) = w.open(1, b"Terminal").unwrap();
|
||||
let (home, _) = w.open(2, b"Home").unwrap();
|
||||
let (app, _) = w.open(2, b"Tally").unwrap();
|
||||
assert!(w.close(app));
|
||||
assert_eq!(w.front(), Some(home), "not the next slot round");
|
||||
w.raise(t);
|
||||
let (app2, _) = w.open(2, b"Snake").unwrap();
|
||||
w.raise(home);
|
||||
w.raise(app2);
|
||||
w.close(app2);
|
||||
assert_eq!(w.front(), Some(home));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! Which windows there are and which is in front (see `types::display::
|
||||
//! window`). Every window fills the screen below the OS bar; one is in
|
||||
//! front at a time, and only it is shown and gets input. A new window comes
|
||||
//! to the front; a key cycles through them. Pure: the server keeps the
|
||||
//! to the front; a key cycles through them. Closing the one in front brings
|
||||
//! back the one in front before it. Pure: the server keeps the
|
||||
//! pixels.
|
||||
|
||||
use types::display::window::{BADGE, NAME_BYTES};
|
||||
@@ -54,6 +55,10 @@ struct Window<N> {
|
||||
accent: Option<u32>,
|
||||
/// Its buffer was asked for: the app runs, so the accent is settled.
|
||||
started: bool,
|
||||
/// What it is (`types::display::window::role`), if it was said.
|
||||
role: Option<usize>,
|
||||
/// When it last came to the front (`Windows::raises`).
|
||||
raised: u64,
|
||||
}
|
||||
|
||||
pub struct Windows<N: Copy> {
|
||||
@@ -61,11 +66,13 @@ pub struct Windows<N: Copy> {
|
||||
front: Option<usize>,
|
||||
/// Makes each window's badge new, so a closed window's never returns.
|
||||
opened: usize,
|
||||
/// Times a window came to the front: what was in front most lately.
|
||||
raises: u64,
|
||||
}
|
||||
|
||||
impl<N: Copy> Windows<N> {
|
||||
pub const fn new() -> Self {
|
||||
Self { slots: [const { None }; MAX_WINDOWS], front: None, opened: 0 }
|
||||
Self { slots: [const { None }; MAX_WINDOWS], front: None, opened: 0, raises: 0 }
|
||||
}
|
||||
|
||||
/// A window for `name`, opened through `opener`'s capability, in
|
||||
@@ -77,8 +84,8 @@ impl<N: Copy> Windows<N> {
|
||||
let mut n = [0u8; NAME_BYTES];
|
||||
let name_len = name.len().min(NAME_BYTES);
|
||||
n[..name_len].copy_from_slice(&name[..name_len]);
|
||||
self.slots[index] = Some(Window { badge, opener, name: n, name_len, notify: None, queue: Queue::new(), accent: None, started: false });
|
||||
self.front = Some(index);
|
||||
self.slots[index] = Some(Window { badge, opener, name: n, name_len, notify: None, queue: Queue::new(), accent: None, started: false, role: None, raised: 0 });
|
||||
self.bring_forward(index);
|
||||
Some((index, badge))
|
||||
}
|
||||
|
||||
@@ -95,8 +102,7 @@ impl<N: Copy> Windows<N> {
|
||||
if self.front != Some(index) {
|
||||
return false;
|
||||
}
|
||||
self.front = None;
|
||||
self.front = self.after(index);
|
||||
self.front = self.slots.iter().enumerate().filter_map(|(i, w)| w.as_ref().map(|w| (w.raised, i))).max().map(|(_, i)| i);
|
||||
true
|
||||
}
|
||||
|
||||
@@ -114,11 +120,19 @@ impl<N: Copy> Windows<N> {
|
||||
pub fn cycle(&mut self) -> Option<usize> {
|
||||
let next = self.after(self.front?)?;
|
||||
(Some(next) != self.front).then(|| {
|
||||
self.front = Some(next);
|
||||
self.bring_forward(next);
|
||||
next
|
||||
})
|
||||
}
|
||||
|
||||
fn bring_forward(&mut self, index: usize) {
|
||||
self.raises += 1;
|
||||
if let Some(w) = self.slots[index].as_mut() {
|
||||
w.raised = self.raises;
|
||||
}
|
||||
self.front = Some(index);
|
||||
}
|
||||
|
||||
pub fn front(&self) -> Option<usize> {
|
||||
self.front
|
||||
}
|
||||
@@ -140,6 +154,40 @@ impl<N: Copy> Windows<N> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Says what the window `badge` is: once, before it starts.
|
||||
pub fn set_role(&mut self, badge: usize, role: usize) -> bool {
|
||||
let Some(w) = self.slot(badge).and_then(|i| self.slots[i].as_mut()) else { return false };
|
||||
let ok = !w.started && w.role.is_none();
|
||||
if ok {
|
||||
w.role = Some(role);
|
||||
}
|
||||
ok
|
||||
}
|
||||
|
||||
pub fn role(&self, index: usize) -> Option<usize> {
|
||||
self.slots.get(index)?.as_ref()?.role
|
||||
}
|
||||
|
||||
/// The first open window that is `role`.
|
||||
pub fn with_role(&self, role: usize) -> Option<usize> {
|
||||
self.slots.iter().position(|w| w.as_ref().is_some_and(|w| w.role == Some(role)))
|
||||
}
|
||||
|
||||
/// Who is told of the window in `index`'s input.
|
||||
pub fn notify(&self, index: usize) -> Option<N> {
|
||||
self.slots.get(index)?.as_ref()?.notify
|
||||
}
|
||||
|
||||
/// Brings the window in `index` to the front; whether it changed.
|
||||
pub fn raise(&mut self, index: usize) -> bool {
|
||||
let open = self.slots.get(index).is_some_and(Option::is_some);
|
||||
let changed = open && self.front != Some(index);
|
||||
if changed {
|
||||
self.bring_forward(index);
|
||||
}
|
||||
changed
|
||||
}
|
||||
|
||||
pub fn accent(&self, badge: usize) -> Option<u32> {
|
||||
self.slots[self.slot(badge)?].as_ref()?.accent
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user