Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better handling for close/suspend/exit #477

Merged
merged 9 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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