Skip to content

Commit

Permalink
Merge pull request #477 from kas-gui/work3
Browse files Browse the repository at this point in the history
Better handling for close/suspend/exit
  • Loading branch information
dhardy authored Feb 17, 2025
2 parents 2efcc4c + f56af3e commit a67910c
Show file tree
Hide file tree
Showing 18 changed files with 160 additions and 108 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ jobs:
toolchain: [stable]
include:
- os: ubuntu-latest
toolchain: "1.80.1"
toolchain: "1.81.0"
variant: MSRV
- os: ubuntu-latest
toolchain: beta
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ keywords = ["gui"]
categories = ["gui"]
repository = "https://github.com/kas-gui/kas"
exclude = ["/examples"]
rust-version = "1.80.1"
rust-version = "1.81.0"

[package.metadata.docs.rs]
features = ["stable"]
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ KAS GUI
[![Crates.io](https://img.shields.io/crates/v/kas.svg)](https://crates.io/crates/kas)
[![kas-text](https://img.shields.io/badge/GitHub-kas--text-blueviolet)](https://github.com/kas-gui/kas-text/)
[![Docs](https://docs.rs/kas/badge.svg)](https://docs.rs/kas)
![Minimum rustc version](https://img.shields.io/badge/rustc-1.80+-lightgray.svg)

KAS is a stateful, pure-Rust GUI toolkit supporting:

Expand Down
2 changes: 0 additions & 2 deletions crates/kas-core/src/action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,5 @@ bitflags! {
const UPDATE = 1 << 17;
/// The current window should be closed
const CLOSE = 1 << 30;
/// Close all windows and exit
const EXIT = 1 << 31;
}
}
21 changes: 6 additions & 15 deletions crates/kas-core/src/event/cx/cx_pub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ impl EventState {
/// Terminate the GUI
#[inline]
pub fn exit(&mut self) {
self.action |= Action::EXIT;
self.pending_cmds.push_back((Id::ROOT, Command::Exit));
}

/// Notify that an [`Action`] should happen
Expand Down Expand Up @@ -849,21 +849,12 @@ impl<'a> EventCx<'a> {
///
/// Navigation focus will return to whichever widget had focus before
/// the popup was open.
pub fn close_window(&mut self, id: WindowId) {
if let Some(index) =
self.popups
.iter()
.enumerate()
.find_map(|(i, p)| if p.0 == id { Some(i) } else { None })
{
let (wid, popup, onf) = self.popups.remove(index);
self.popup_removed.push((popup.id, wid));
self.runner.close_window(wid);

if let Some(id) = onf {
self.set_nav_focus(id, FocusSource::Synthetic);
pub fn close_window(&mut self, mut id: WindowId) {
for (index, p) in self.popups.iter().enumerate() {
if p.0 == id {
id = self.close_popup(index);
break;
}
return;
}

self.runner.close_window(id);
Expand Down
41 changes: 37 additions & 4 deletions crates/kas-core/src/event/cx/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ type AccessLayer = (bool, HashMap<Key, Id>);
// for each widget during drawing. Most fields contain only a few values, hence
// `SmallVec` is used to keep contents in local memory.
pub struct EventState {
pub(crate) window_id: WindowId,
config: WindowConfig,
platform: Platform,
disabled: Vec<Id>,
Expand Down Expand Up @@ -362,6 +363,23 @@ impl EventState {
grab
}

// Remove popup at index and return its [`WindowId`]
//
// Panics if `index` is out of bounds.
//
// The caller must call `runner.close_window(window_id)`.
#[must_use]
fn close_popup(&mut self, index: usize) -> WindowId {
let (window_id, popup, onf) = self.popups.remove(index);
self.popup_removed.push((popup.id, window_id));

if let Some(id) = onf {
self.set_nav_focus(id, FocusSource::Synthetic);
}

window_id
}

/// Clear all active events on `target`
fn clear_events(&mut self, target: &Id) {
if let Some(id) = self.sel_focus.as_ref() {
Expand Down Expand Up @@ -446,9 +464,15 @@ impl<'a> EventCx<'a> {
widget.id()
);

let opt_command = self.config.shortcuts().try_match(self.modifiers, &vkey);
let opt_cmd = self.config.shortcuts().try_match(self.modifiers, &vkey);

if let Some(cmd) = opt_command {
if Some(Command::Exit) == opt_cmd {
self.runner.exit();
return;
} else if Some(Command::Close) == opt_cmd {
self.handle_close();
return;
} else if let Some(cmd) = opt_cmd {
let mut targets = vec![];
let mut send = |_self: &mut Self, id: Id, cmd| -> bool {
if !targets.contains(&id) {
Expand Down Expand Up @@ -524,10 +548,10 @@ impl<'a> EventCx<'a> {
}
let event = Event::Command(Command::Activate, Some(code));
self.send_event(widget, id, event);
} else if self.config.nav_focus && vkey == Key::Named(NamedKey::Tab) {
} else if self.config.nav_focus && opt_cmd == Some(Command::Tab) {
let shift = self.modifiers.shift_key();
self.next_nav_focus_impl(widget.re(), None, shift, FocusSource::Key);
} else if vkey == Key::Named(NamedKey::Escape) {
} else if opt_cmd == Some(Command::Escape) {
if let Some(id) = self.popups.last().map(|(id, _, _)| *id) {
self.close_window(id);
}
Expand Down Expand Up @@ -627,6 +651,15 @@ impl<'a> EventCx<'a> {
}
}

fn handle_close(&mut self) {
let mut id = self.window_id;
if !self.popups.is_empty() {
let index = self.popups.len() - 1;
id = self.close_popup(index);
}
self.runner.close_window(id);
}

// Call Widget::_nav_next
#[inline]
fn nav_next(
Expand Down
24 changes: 19 additions & 5 deletions crates/kas-core/src/event/cx/platform.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ const FAKE_MOUSE_BUTTON: MouseButton = MouseButton::Other(0);
impl EventState {
/// Construct per-window event state
#[inline]
pub(crate) fn new(config: WindowConfig, platform: Platform) -> Self {
pub(crate) fn new(window_id: WindowId, config: WindowConfig, platform: Platform) -> Self {
EventState {
window_id,
config,
platform,
disabled: vec![],
Expand Down Expand Up @@ -78,11 +79,10 @@ impl EventState {
pub(crate) fn full_configure<A>(
&mut self,
sizer: &dyn ThemeSize,
wid: WindowId,
win: &mut Window<A>,
data: &A,
) {
let id = Id::ROOT.make_child(wid.get().cast());
let id = Id::ROOT.make_child(self.window_id.get().cast());

log::debug!(target: "kas_core::event", "full_configure of Window{id}");
self.action.remove(Action::RECONFIGURE);
Expand Down Expand Up @@ -248,8 +248,14 @@ impl EventState {
}

while let Some((id, cmd)) = cx.pending_cmds.pop_front() {
log::trace!(target: "kas_core::event", "sending pending command {cmd:?} to {id}");
cx.send_event(win.as_node(data), id, Event::Command(cmd, None));
if cmd == Command::Exit {
cx.runner.exit();
} else if cmd == Command::Close {
cx.handle_close();
} else {
log::trace!(target: "kas_core::event", "sending pending command {cmd:?} to {id}");
cx.send_event(win.as_node(data), id, Event::Command(cmd, None));
}
}

while let Some((id, msg)) = cx.send_queue.pop_front() {
Expand Down Expand Up @@ -282,6 +288,14 @@ impl EventState {

std::mem::take(&mut self.action)
}

/// Window has been closed: clean up state
pub(crate) fn suspended(&mut self, runner: &mut dyn RunnerT) {
while !self.popups.is_empty() {
let id = self.close_popup(self.popups.len() - 1);
runner.close_window(id);
}
}
}

/// Platform API
Expand Down
6 changes: 3 additions & 3 deletions crates/kas-core/src/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -401,21 +401,21 @@ impl<Data: 'static> Window<Data> {
/// Each [`crate::Popup`] is assigned a [`WindowId`]; both are passed.
pub(crate) fn add_popup(
&mut self,
cx: &mut EventCx,
cx: &mut ConfigCx,
data: &Data,
id: WindowId,
popup: kas::PopupDescriptor,
) {
let index = self.popups.len();
self.popups.push((id, popup, Offset::ZERO));
self.resize_popup(&mut cx.config_cx(), data, index);
self.resize_popup(cx, data, index);
cx.action(Id::ROOT, Action::REDRAW);
}

/// Trigger closure of a pop-up
///
/// If the given `id` refers to a pop-up, it should be closed.
pub(crate) fn remove_popup(&mut self, cx: &mut EventCx, id: WindowId) {
pub(crate) fn remove_popup(&mut self, cx: &mut ConfigCx, id: WindowId) {
for i in 0..self.popups.len() {
if id == self.popups[i].0 {
self.popups.remove(i);
Expand Down
41 changes: 20 additions & 21 deletions crates/kas-core/src/runner/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ where
for window in self.windows.values_mut() {
match window.resume(&mut self.state, el) {
Ok(winit_id) => {
self.id_map.insert(winit_id, window.window_id);
self.id_map.insert(winit_id, window.window_id());
}
Err(e) => {
log::error!("Unable to create window: {}", e);
Expand Down Expand Up @@ -152,15 +152,15 @@ where

fn suspended(&mut self, _: &ActiveEventLoop) {
if !self.suspended {
for window in self.windows.values_mut() {
window.suspend();
}
self.windows
.retain(|_, window| window.suspend(&mut self.state));
self.state.suspended();
self.suspended = true;
}
}

fn exiting(&mut self, _: &ActiveEventLoop) {
self.state.on_exit();
fn exiting(&mut self, el: &ActiveEventLoop) {
self.suspended(el);
}
}

Expand All @@ -171,7 +171,7 @@ where
pub(super) fn new(mut windows: Vec<Box<Window<A, G, T>>>, state: State<A, G, T>) -> Self {
Loop {
suspended: true,
windows: windows.drain(..).map(|w| (w.window_id, w)).collect(),
windows: windows.drain(..).map(|w| (w.window_id(), w)).collect(),
popups: Default::default(),
id_map: Default::default(),
state,
Expand All @@ -180,6 +180,7 @@ where
}

fn flush_pending(&mut self, el: &ActiveEventLoop) {
let mut close_all = false;
while let Some(pending) = self.state.shared.pending.pop_front() {
match pending {
Pending::AddPopup(parent_id, id, popup) => {
Expand Down Expand Up @@ -211,11 +212,11 @@ where
win_id = id;
}
if let Some(window) = self.windows.get_mut(&win_id) {
window.send_close(&mut self.state, target);
window.send_close(target);
}
}
Pending::Action(action) => {
if action.contains(Action::CLOSE | Action::EXIT) {
if action.contains(Action::CLOSE) {
self.windows.clear();
self.id_map.clear();
el.set_control_flow(ControlFlow::Poll);
Expand All @@ -225,32 +226,30 @@ where
}
}
}
Pending::Exit => close_all = true,
}
}

let mut close_all = false;
self.resumes.clear();
self.windows.retain(|window_id, window| {
let (action, resume) = window.flush_pending(&mut self.state);
if let Some(instant) = resume {
self.resumes.push((instant, *window_id));
}
if action.contains(Action::EXIT) {
close_all = true;
true
} else if action.contains(Action::CLOSE) {

if close_all || action.contains(Action::CLOSE) {
window.suspend(&mut self.state);

// Call flush_pending again since suspend may queue messages.
// We don't care about the returned Action or resume times since
// the window is being destroyed.
let _ = window.flush_pending(&mut self.state);

self.id_map.retain(|_, v| v != window_id);
false
} else {
true
}
});

if close_all {
for (_, mut window) in self.windows.drain() {
window.suspend();
}
self.id_map.clear();
}
}
}
16 changes: 16 additions & 0 deletions crates/kas-core/src/runner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,32 @@ pub extern crate raw_window_handle;
/// across windows). This trait must be implemented by the latter.
///
/// When no top-level data is required, use `()` which implements this trait.
///
/// TODO: should we pass some type of interface to the runner to these methods?
/// We could pass a `&mut dyn RunnerT` easily, but that trait is not public.
pub trait AppData: 'static {
/// Handle messages
///
/// This is the last message handler: it is called when, after traversing
/// the widget tree (see [kas::event] module doc), a message is left on the
/// stack. Unhandled messages will result in warnings in the log.
fn handle_messages(&mut self, messages: &mut MessageStack);

/// Application is being suspended
///
/// The application should ensure any important state is saved.
///
/// This method is called when the application has been suspended or is
/// about to exit (on Android/iOS/Web platforms, the application may resume
/// after this method is called; on other platforms this probably indicates
/// imminent closure). Widget state may still exist, but is not live
/// (widgets will not process events or messages).
fn suspended(&mut self) {}
}

impl AppData for () {
fn handle_messages(&mut self, _: &mut MessageStack) {}
fn suspended(&mut self) {}
}

#[crate::autoimpl(Debug)]
Expand All @@ -61,6 +76,7 @@ enum Pending<A: AppData, G: GraphicsBuilder, T: kas::theme::Theme<G::Shared>> {
AddWindow(WindowId, Box<Window<A, G, T>>),
CloseWindow(WindowId),
Action(kas::Action),
Exit,
}

#[cfg(winit)]
Expand Down
Loading

0 comments on commit a67910c

Please sign in to comment.