mirror of
https://github.com/devmobasa/wayscriber.git
synced 2026-11-01 07:36:42 +01:00
feat(session): return to the home session from the Session menu
The Session popover gains a way back to the home session: "Back to <session>" when the overlay or its daemon started with a session file, or "Default session" without one. It is disabled while home is already active, and the side.session.home toolbar item hides it. The label and destination come from the launch's home input, not from the session the overlay is in. Returning home is an explicit session command. Like Open, it saves dirty current data first, refuses if the canvas or interaction changes while it is pending, and replaces nothing unless home loads. Home loads the way a launch would, from its own options for the current output, so recovery, clear-boundary and tool-restore rules apply, and its load errors stay those of a startup session file rather than becoming an empty session. A home with persistence disabled continues unsaved on an empty canvas. Once home is committed the overlay reports it, so the daemon forgets the session it remembered. The overlay keeps whether it is home with its reported target, so the menu needs no file system check on redraw.
This commit is contained in:
1 parent
f4109dd5ba
commit
d5baaaddce
31 files changed
+553
-40
No files matched your search
@@ -1212,7 +1212,7 @@ See [Session manager examples](examples/session-manager.md) for complete CLI, ov
|
||||
- When a fresh launch restores ink onto the transparent overlay board, a toast such as "Restored 7 annotations from last session" offers **Clear** (undoable, like Clear Canvas), because that ink now sits over whatever is on screen. Daemon toggles, empty restores, and solid boards stay quiet.
|
||||
- Config values seed startup defaults. When `restore_tool_state` is enabled (default), the last-used tool settings saved in the session (including arrow head placement and the starting Spotlight magnification) override those config defaults on startup. Run `wayscriber --clear-tool-state` to remove only that saved tool layer so config defaults apply next startup while saved boards/history remain. In a running overlay, use Command Palette → Reset Tool Defaults to clear the saved layer and immediately apply config defaults to the active tools.
|
||||
- `--session-file` uses exactly the selected file, implies persistence for that overlay run, rejects directories/symlinks/special files, and does not create missing parent directories. A running daemon can launch a hidden overlay with a named target; if the overlay is already visible, hide it before switching to a different named session. The daemon's overlay keeps the session it last opened or saved as across hide and show, while the daemon runs.
|
||||
- The overlay Session controls live in the top toolbar's Session popover (overflow menu → Session...). They can open an existing named session, save the current overlay as another named session, show session info, clear the active session, reopen recent named sessions, and jump to the configurator. The Open/Save As dialogs use `zenity` or `kdialog`; Save As appends `.wayscriber-session` when no extension is supplied and asks before replacing existing session artifacts.
|
||||
- The overlay Session controls live in the top toolbar's Session popover (overflow menu → Session...). They can open an existing named session, save the current overlay as another named session, return to the home session (**Back to** the startup session file, or **Default session**), show session info, clear the active session, reopen recent named sessions, and jump to the configurator. The Open/Save As dialogs use `zenity` or `kdialog`; Save As appends `.wayscriber-session` when no extension is supplied and asks before replacing existing session artifacts.
|
||||
- The configurator Session tab manages recent named sessions recorded when named-session targets are opened or saved from the CLI, daemon, or overlay. It can rename catalog labels, reveal files, and forget metadata without touching files. Clear Tool State removes only the saved tool layer; Clear Saved Data removes session files. Duplicate, Move, Clear Tool State, and Clear are disabled while an overlay, manually started daemon, or background service is active.
|
||||
|
||||
</details>
|
||||
|
||||
+2
-1
@@ -2114,7 +2114,7 @@ Use the CLI helpers for quick maintenance:
|
||||
- `wayscriber --daemon --session-file ~/Documents/lecture-04.wayscriber-session` starts a daemon whose overlay activations use that named session target, the daemon's home session.
|
||||
- `wayscriber --daemon-toggle --session-file ~/Documents/meeting.wayscriber-session` asks the running daemon to launch a hidden overlay with that named session target. It applies to that activation only. If the overlay is already visible with a different target, hide it before switching.
|
||||
|
||||
The daemon's overlay keeps its session across hide and show. After **Open** or **Save As** switches the overlay to another session, the next activation continues in that session, until the overlay returns to the daemon's home session: the file given with `--daemon --session-file`, or the configured default session without one. A `--daemon-toggle --session-file` request takes precedence for its activation. If the remembered file was moved or deleted, the overlay opens the home session instead and says so. The daemon keeps this only while it runs; a restarted daemon starts at home.
|
||||
The daemon's overlay keeps its session across hide and show. After **Open** or **Save As** switches the overlay to another session, the next activation continues in that session, until the overlay returns to the daemon's home session: the file given with `--daemon --session-file`, or the configured default session without one. The Session popover's **Back to <session>** or **Default session** button returns home. A `--daemon-toggle --session-file` request takes precedence for its activation. If the remembered file was moved or deleted, the overlay opens the home session instead and says so. The daemon keeps this only while it runs; a restarted daemon starts at home.
|
||||
- `wayscriber --session-info --session-file <path>`, `wayscriber --clear-session --session-file <path>`, and `wayscriber --clear-tool-state --session-file <path>` target only that named file.
|
||||
|
||||
Config values seed startup defaults. When `restore_tool_state = true`, the saved session tool state is applied after those defaults, so edits such as `[arrow] head_at_end = true` can appear ignored if the session snapshot still stores an older arrow setting. Run `wayscriber --clear-tool-state` (or add `--session-file <path>` for a named session) to make config defaults apply on the next startup without deleting saved boards. In a running overlay, Command Palette -> Reset Tool Defaults clears the saved layer for the active session and immediately applies config defaults to the current tools so the next autosave keeps those defaults.
|
||||
@@ -2127,6 +2127,7 @@ The overlay Session panel lives in the top strip's overflow **"Session..."** pop
|
||||
- `Save As` writes the current overlay to another named session and switches the active target. It appends `.wayscriber-session` when no extension is supplied and asks before replacing existing session artifacts.
|
||||
- `Info` reports the active session file size, board shape counts, and history status.
|
||||
- `Clear` writes a durable empty session boundary for the active target.
|
||||
- `Back to <session>` returns to the home session: the file the overlay or daemon started with. Without one it reads `Default session` and returns to the configured default session. Like `Open`, it saves dirty current data first and switches only once home has loaded. It is disabled while home is already active. Hide it with the `side.session.home` toolbar item.
|
||||
- Recent session rows reopen other named sessions. If a recent target is missing, Wayscriber removes that stale catalog entry after the failed open.
|
||||
- `Manager` opens the configurator. Overlay Open/Save As dialogs use `zenity` or `kdialog`.
|
||||
|
||||
|
||||
@@ -539,18 +539,20 @@ capture suppression operates on the paired resources without runtime pairing che
|
||||
**Modules:**
|
||||
- `src/session/`: target options, primary-file validation, snapshot load/save, sidecars, clear/recovery markers, saved tool-state reset, locks, catalog metadata, and inactive file operations.
|
||||
- `src/backend/wayland/session/`: runtime Open, Save As, Clear, and saved tool-state reset transactions for the active overlay.
|
||||
- `src/backend/wayland/state/toolbar/events/session.rs`: overlay Session popover routing for Open, Save As, Info, Clear, recent sessions, and configurator launch.
|
||||
- `src/daemon/`: accepts daemon-toggle requests that carry an optional named session target.
|
||||
- `src/backend/wayland/state/toolbar/events/session.rs`: overlay Session popover routing for Open, Save As, return home, Info, Clear, recent sessions, and configurator launch.
|
||||
- `src/backend/wayland/session/home.rs`: the overlay's home session, the remembered session a daemon launch carries, and the session reports sent back to the daemon.
|
||||
- `src/daemon/`: accepts daemon-toggle requests that carry an optional named session target, and remembers the session its overlay last reported across hide and show.
|
||||
|
||||
**Flow:**
|
||||
1. CLI `--session-file` creates a named target instead of using configured storage. Named targets force persistence for that run, reject `--no-resume-session`, require an existing parent directory for foreground/open flows, and reject directories, symlinks, and special files.
|
||||
2. Backend startup builds `SessionOptions` from config plus any named target, then session loading restores boards/history/tool state before rendering begins.
|
||||
3. Runtime Open first saves dirty current data when needed, loads the candidate named session without mutating it, replaces board state only after a valid load, and records the open in the named-session catalog.
|
||||
4. Runtime Save As validates the target, prompts before replacing existing artifacts, writes the snapshot, switches the active target, and records the save in the catalog.
|
||||
5. Runtime Clear writes a durable empty-session boundary so older backup or recovery artifacts do not restore stale drawings.
|
||||
6. Runtime saved tool-state reset clears the persisted tool layer for the active session and applies config-derived tool defaults in memory so autosave does not restore stale values.
|
||||
7. Offline CLI maintenance can inspect sessions, clear all saved data, or clear only persisted tool state so config defaults seed the next startup without deleting boards.
|
||||
8. The configurator reads the same catalog for inactive-session management: rename/reveal/forget metadata, duplicate primary files, move non-lock sidecars, clear saved tool state, and clear saved data when daemon/overlay locks are absent.
|
||||
5. Returning home saves dirty current data the same way, then loads the home session as a launch would. A daemon overlay reports each committed target change, so the daemon starts the next overlay in that session; a remembered session that no longer exists falls back to home.
|
||||
6. Runtime Clear writes a durable empty-session boundary so older backup or recovery artifacts do not restore stale drawings.
|
||||
7. Runtime saved tool-state reset clears the persisted tool layer for the active session and applies config-derived tool defaults in memory so autosave does not restore stale values.
|
||||
8. Offline CLI maintenance can inspect sessions, clear all saved data, or clear only persisted tool state so config defaults seed the next startup without deleting boards.
|
||||
9. The configurator reads the same catalog for inactive-session management: rename/reveal/forget metadata, duplicate primary files, move non-lock sidecars, clear saved tool state, and clear saved data when daemon/overlay locks are absent.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -47,6 +47,9 @@ Open Wayscriber with any persisted session target, then use the top strip overfl
|
||||
- `Info` reports the active session file size, board shape counts, and history
|
||||
status.
|
||||
- `Clear` writes a durable empty session boundary for the active target.
|
||||
- `Back to <session>`, or `Default session` without a startup session file,
|
||||
saves the current session and returns to the session the overlay or daemon
|
||||
started with. It is disabled while that session is already active.
|
||||
- Recent session rows reopen other named sessions.
|
||||
- `Manager` opens the configurator.
|
||||
|
||||
|
||||
@@ -227,6 +227,16 @@ impl SessionState {
|
||||
self.notified_failure = false;
|
||||
}
|
||||
|
||||
/// Continues without persistence: home is a default session that is not
|
||||
/// saved.
|
||||
pub(in crate::backend::wayland) fn commit_without_persistence(&mut self) {
|
||||
self.advance_target_epoch();
|
||||
self.options = None;
|
||||
self.loaded = true;
|
||||
self.loaded_board_data = false;
|
||||
self.mark_clean_after_load();
|
||||
}
|
||||
|
||||
pub(in crate::backend::wayland) fn commit_runtime_clear(&mut self, now: Instant) {
|
||||
self.loaded = true;
|
||||
self.loaded_board_data = false;
|
||||
@@ -595,6 +605,8 @@ mod home;
|
||||
mod persistence;
|
||||
mod runtime;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(in crate::backend::wayland) use home::HomeSession;
|
||||
pub(in crate::backend::wayland) use home::{SessionHome, SessionLaunch, session_target};
|
||||
pub(in crate::backend::wayland) use persistence::{
|
||||
PersistenceCompletion, PersistenceController, PersistenceOperation, PersistenceOutcome,
|
||||
@@ -602,8 +614,8 @@ pub(in crate::backend::wayland) use persistence::{
|
||||
};
|
||||
|
||||
pub(in crate::backend::wayland) use runtime::{
|
||||
ExplicitSessionTransaction, SessionCommand, SessionCommandReport, SessionTransaction,
|
||||
TransactionStep,
|
||||
ExplicitSessionTransaction, RuntimeHomeSessionReport, SessionCommand, SessionCommandReport,
|
||||
SessionTransaction, TransactionStep,
|
||||
};
|
||||
#[cfg(test)]
|
||||
pub(in crate::backend::wayland) use runtime::{
|
||||
|
||||
@@ -707,6 +707,104 @@ fn failed_open_and_save_as_announce_nothing() {
|
||||
}
|
||||
}
|
||||
|
||||
fn configured_home(base: &std::path::Path) -> stored_session::SessionOptions {
|
||||
let mut options = stored_session::SessionOptions::new(base.join("configured"), "home");
|
||||
options.persist_transparent = true;
|
||||
options
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returning_home_saves_the_current_session_then_loads_home_like_a_launch() {
|
||||
let temp = crate::test_temp::tempdir().unwrap();
|
||||
let current = named_options(temp.path(), "current");
|
||||
let home = configured_home(temp.path());
|
||||
stored_session::save_snapshot(&sample_snapshot(), &home).unwrap();
|
||||
let mut input = test_input_state();
|
||||
add_line(&mut input, 51);
|
||||
input.mark_session_dirty();
|
||||
let mut session = SessionState::new(Some(current.clone()));
|
||||
let measurer = TextMeasurer::default();
|
||||
let (persistence, worker) = PersistenceController::controlled_for_test();
|
||||
let mut runtime = CommandRuntime::new(&mut input, &measurer, &mut session, persistence);
|
||||
|
||||
start_session_command(
|
||||
&mut runtime,
|
||||
SessionCommand::OpenHome(Some(Box::new(home.clone()))),
|
||||
)
|
||||
.unwrap();
|
||||
worker.complete_next(); // save the current session
|
||||
runtime.receive();
|
||||
assert_eq!(loaded_line_x2(¤t), 51);
|
||||
assert!(runtime.reports.is_empty());
|
||||
worker.complete_next(); // load home
|
||||
runtime.receive();
|
||||
|
||||
assert!(runtime.errors.is_empty());
|
||||
let [SessionCommandReport::Home(report)] = runtime.reports.as_slice() else {
|
||||
panic!(
|
||||
"expected a home report, got {} reports",
|
||||
runtime.reports.len()
|
||||
);
|
||||
};
|
||||
assert_eq!(
|
||||
report.options.as_ref().map(|options| &options.target),
|
||||
Some(&home.target)
|
||||
);
|
||||
assert!(matches!(
|
||||
report.outcome,
|
||||
Some(stored_session::LoadSnapshotOutcome::Loaded(_))
|
||||
));
|
||||
// The runtime applies home; until then the canvas and target stay put.
|
||||
assert_eq!(runtime.session.options().unwrap().target, current.target);
|
||||
assert_eq!(runtime.input.boards.active_frame().shapes.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returning_home_is_refused_when_the_canvas_changes_while_home_loads() {
|
||||
let temp = crate::test_temp::tempdir().unwrap();
|
||||
let current = named_options(temp.path(), "current");
|
||||
let home = configured_home(temp.path());
|
||||
let mut input = test_input_state();
|
||||
let mut session = SessionState::new(Some(current.clone()));
|
||||
let measurer = TextMeasurer::default();
|
||||
let (persistence, worker) = PersistenceController::controlled_for_test();
|
||||
let mut runtime = CommandRuntime::new(&mut input, &measurer, &mut session, persistence);
|
||||
|
||||
start_session_command(&mut runtime, SessionCommand::OpenHome(Some(Box::new(home)))).unwrap();
|
||||
add_line(runtime.input, 77);
|
||||
runtime.input.mark_session_dirty();
|
||||
worker.complete_next(); // load home
|
||||
runtime.receive();
|
||||
|
||||
assert!(runtime.reports.is_empty());
|
||||
assert!(
|
||||
runtime.errors[0]
|
||||
.to_string()
|
||||
.contains("session was edited while the command was pending"),
|
||||
"{:?}",
|
||||
runtime.errors
|
||||
);
|
||||
assert_eq!(runtime.session.options().unwrap().target, current.target);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returning_to_a_home_without_persistence_loads_nothing() {
|
||||
let temp = crate::test_temp::tempdir().unwrap();
|
||||
let mut input = test_input_state();
|
||||
let mut session = SessionState::new(Some(named_options(temp.path(), "current")));
|
||||
let measurer = TextMeasurer::default();
|
||||
let (persistence, worker) = PersistenceController::controlled_for_test();
|
||||
let mut runtime = CommandRuntime::new(&mut input, &measurer, &mut session, persistence);
|
||||
|
||||
start_session_command(&mut runtime, SessionCommand::OpenHome(None)).unwrap();
|
||||
|
||||
assert!(!worker.has_request());
|
||||
let [SessionCommandReport::Home(report)] = runtime.reports.as_slice() else {
|
||||
panic!("expected a home report");
|
||||
};
|
||||
assert!(report.options.is_none() && report.outcome.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_completion_refreshes_consumer_seeds() {
|
||||
let temp = crate::test_temp::tempdir().unwrap();
|
||||
|
||||
@@ -90,6 +90,9 @@ pub(in crate::backend::wayland) struct SessionHome {
|
||||
/// The target the daemon last learned this overlay is in, by launching it
|
||||
/// there or from a report.
|
||||
reported: SessionTarget,
|
||||
/// Whether `reported` is home. Kept rather than worked out when asked,
|
||||
/// since the Session menu asks on every redraw.
|
||||
at_home: bool,
|
||||
}
|
||||
|
||||
impl SessionHome {
|
||||
@@ -100,6 +103,7 @@ impl SessionHome {
|
||||
startup: SessionTarget,
|
||||
) -> Self {
|
||||
Self {
|
||||
at_home: is_home(&launch.home, &startup),
|
||||
home: launch.home,
|
||||
options,
|
||||
unchecked_preferred: launch.preferred,
|
||||
@@ -117,38 +121,50 @@ impl SessionHome {
|
||||
self.unchecked_preferred.take()
|
||||
}
|
||||
|
||||
/// How the Session menu and notices name home.
|
||||
pub(in crate::backend::wayland) fn label(&self) -> String {
|
||||
match &self.home {
|
||||
HomeSession::Default => "the default session".to_owned(),
|
||||
HomeSession::Named(path) => session_display_name(path),
|
||||
}
|
||||
/// The name of a named home session; `None` for the default session.
|
||||
pub(in crate::backend::wayland) fn name(&self) -> Option<String> {
|
||||
self.home.file().map(session_display_name)
|
||||
}
|
||||
|
||||
/// What to report now that the overlay is in `target`, or `None` when the
|
||||
/// daemon already knows. Home is reported as such even when it is a named
|
||||
/// file, so the daemon never remembers home as a session of its own.
|
||||
pub(in crate::backend::wayland) fn report_for(
|
||||
/// How notices name home.
|
||||
pub(in crate::backend::wayland) fn label(&self) -> String {
|
||||
self.name()
|
||||
.unwrap_or_else(|| "the default session".to_owned())
|
||||
}
|
||||
|
||||
/// Whether the overlay is in its home session, as of the last commit.
|
||||
pub(in crate::backend::wayland) fn is_at_home(&self) -> bool {
|
||||
self.at_home
|
||||
}
|
||||
|
||||
/// Records that the overlay is now in `target`, returning what to report,
|
||||
/// or `None` when the daemon already knows. Home is reported as such even
|
||||
/// when it is a named file, so the daemon never remembers home as a
|
||||
/// session of its own.
|
||||
pub(in crate::backend::wayland) fn commit_target(
|
||||
&mut self,
|
||||
target: SessionTarget,
|
||||
) -> Option<ReportedSession> {
|
||||
if target == self.reported {
|
||||
return None;
|
||||
}
|
||||
self.at_home = is_home(&self.home, &target);
|
||||
let report = match &target {
|
||||
SessionTarget::NamedFile(path) if !self.is_home_file(path) => {
|
||||
ReportedSession::Named(path.clone())
|
||||
}
|
||||
SessionTarget::NamedFile(path) if !self.at_home => ReportedSession::Named(path.clone()),
|
||||
_ => ReportedSession::Home,
|
||||
};
|
||||
self.reported = target;
|
||||
Some(report)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_home_file(&self, path: &Path) -> bool {
|
||||
self.home
|
||||
.file()
|
||||
.is_some_and(|home| session_paths_match(home, path))
|
||||
fn is_home(home: &HomeSession, target: &SessionTarget) -> bool {
|
||||
match (home, target) {
|
||||
(HomeSession::Default, SessionTarget::Configured) => true,
|
||||
(HomeSession::Named(home), SessionTarget::NamedFile(path)) => {
|
||||
session_paths_match(home, path)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -133,14 +133,14 @@ fn only_the_first_load_checks_the_preferred_session() {
|
||||
fn only_a_changed_session_is_reported() {
|
||||
let mut home = home_session(named(HOME), Some(PREFERRED), file(PREFERRED));
|
||||
|
||||
assert_eq!(home.report_for(file(PREFERRED)), None);
|
||||
assert_eq!(home.commit_target(file(PREFERRED)), None);
|
||||
|
||||
let other = "/sessions/d.wayscriber-session";
|
||||
assert_eq!(
|
||||
home.report_for(file(other)),
|
||||
home.commit_target(file(other)),
|
||||
Some(ReportedSession::Named(PathBuf::from(other)))
|
||||
);
|
||||
assert_eq!(home.report_for(file(other)), None);
|
||||
assert_eq!(home.commit_target(file(other)), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -150,17 +150,17 @@ fn home_is_reported_as_home_even_as_a_named_file() {
|
||||
let alias = "/sessions/./home.wayscriber-session";
|
||||
|
||||
assert_eq!(
|
||||
named_home.report_for(file(alias)),
|
||||
named_home.commit_target(file(alias)),
|
||||
Some(ReportedSession::Home)
|
||||
);
|
||||
|
||||
let mut default_home = home_session(HomeSession::Default, None, file(PREFERRED));
|
||||
assert_eq!(
|
||||
default_home.report_for(SessionTarget::Configured),
|
||||
default_home.commit_target(SessionTarget::Configured),
|
||||
Some(ReportedSession::Home)
|
||||
);
|
||||
assert_eq!(
|
||||
default_home.report_for(file(HOME)),
|
||||
default_home.commit_target(file(HOME)),
|
||||
Some(ReportedSession::Named(PathBuf::from(HOME)))
|
||||
);
|
||||
}
|
||||
@@ -169,3 +169,34 @@ fn home_is_reported_as_home_even_as_a_named_file() {
|
||||
fn a_run_without_persistence_is_in_the_default_session() {
|
||||
assert_eq!(session_target(None), SessionTarget::Configured);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_overlay_knows_whether_it_is_home() {
|
||||
let mut named_home = home_session(named(HOME), Some(PREFERRED), file(PREFERRED));
|
||||
assert!(!named_home.is_at_home());
|
||||
named_home.commit_target(file("/sessions/./home.wayscriber-session"));
|
||||
assert!(named_home.is_at_home());
|
||||
named_home.commit_target(file(PREFERRED));
|
||||
assert!(!named_home.is_at_home());
|
||||
assert!(home_session(named(HOME), None, file(HOME)).is_at_home());
|
||||
|
||||
let mut default_home = home_session(HomeSession::Default, None, file(PREFERRED));
|
||||
assert!(!default_home.is_at_home());
|
||||
default_home.commit_target(SessionTarget::Configured);
|
||||
assert!(default_home.is_at_home());
|
||||
assert!(home_session(HomeSession::Default, None, SessionTarget::Configured).is_at_home());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_a_named_home_has_a_name() {
|
||||
assert_eq!(
|
||||
home_session(named(HOME), None, file(HOME))
|
||||
.name()
|
||||
.as_deref(),
|
||||
Some("home.wayscriber-session")
|
||||
);
|
||||
assert_eq!(
|
||||
home_session(HomeSession::Default, None, SessionTarget::Configured).name(),
|
||||
None
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,16 @@ pub(in crate::backend::wayland) struct RuntimeOpenSessionReport {
|
||||
pub catalog_error: Option<anyhow::Error>,
|
||||
}
|
||||
|
||||
/// A return to the home session that saved the current session and loaded
|
||||
/// home, for the runtime to apply.
|
||||
#[derive(Debug)]
|
||||
pub(in crate::backend::wayland) struct RuntimeHomeSessionReport {
|
||||
/// Home's options; `None` when home has persistence disabled.
|
||||
pub options: Option<SessionOptions>,
|
||||
/// What loading home found; `None` without persistence.
|
||||
pub outcome: Option<LoadSnapshotOutcome>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(in crate::backend::wayland) struct RuntimeSaveAsSessionReport {
|
||||
|
||||
@@ -6,6 +6,9 @@ mod phases;
|
||||
#[derive(Debug)]
|
||||
pub(in crate::backend::wayland) enum SessionCommand {
|
||||
Open(PathBuf),
|
||||
/// Return to the home session, whose options for the current output this
|
||||
/// carries; `None` when home has persistence disabled.
|
||||
OpenHome(Option<Box<SessionOptions>>),
|
||||
SaveAs(PathBuf, SaveAsOverwrite),
|
||||
CheckOverwrite(PathBuf),
|
||||
Clear,
|
||||
@@ -16,6 +19,7 @@ pub(in crate::backend::wayland) enum SessionCommand {
|
||||
|
||||
pub(in crate::backend::wayland) enum SessionCommandReport {
|
||||
Open(RuntimeOpenSessionReport),
|
||||
Home(RuntimeHomeSessionReport),
|
||||
SaveAs(RuntimeSaveAsSessionReport),
|
||||
Overwrite(PathBuf, bool),
|
||||
Clear(RuntimeClearSessionReport),
|
||||
@@ -36,6 +40,7 @@ enum Phase {
|
||||
SaveCurrent,
|
||||
Load,
|
||||
RecordOpen,
|
||||
LoadHome,
|
||||
SaveAsPreflight,
|
||||
SaveAs,
|
||||
Clear,
|
||||
@@ -117,6 +122,7 @@ impl ExplicitSessionTransaction {
|
||||
Phase::SaveCurrent => self.complete_save_current(context, outcome),
|
||||
Phase::Load => self.complete_load(context, outcome),
|
||||
Phase::RecordOpen => self.complete_record_open(context, outcome),
|
||||
Phase::LoadHome => self.complete_load_home(context, outcome),
|
||||
Phase::SaveAsPreflight => self.complete_save_as_preflight(context, outcome),
|
||||
Phase::SaveAs => self.complete_save_as(context, outcome),
|
||||
Phase::Clear => self.complete_clear(context, outcome),
|
||||
@@ -196,6 +202,22 @@ impl ExplicitSessionTransaction {
|
||||
},
|
||||
)
|
||||
}
|
||||
// Home loads the way a launch would, so its recovery, clear and
|
||||
// tool-restore rules apply rather than those of a runtime Open.
|
||||
SessionCommand::OpenHome(Some(options)) => {
|
||||
let options = (**options).clone();
|
||||
self.capture_input_generation(context);
|
||||
self.work(
|
||||
Phase::LoadHome,
|
||||
PersistenceOperation::LoadConfigured { options },
|
||||
)
|
||||
}
|
||||
SessionCommand::OpenHome(None) => Ok(TransactionStep::Complete(Box::new(
|
||||
SessionCommandReport::Home(RuntimeHomeSessionReport {
|
||||
options: None,
|
||||
outcome: None,
|
||||
}),
|
||||
))),
|
||||
SessionCommand::SaveAs(_, _) => Ok(TransactionStep::Complete(Box::new(
|
||||
SessionCommandReport::SaveAs(RuntimeSaveAsSessionReport {
|
||||
previous_path: self.current_path(),
|
||||
|
||||
@@ -57,6 +57,13 @@ impl ExplicitSessionTransaction {
|
||||
},
|
||||
)
|
||||
}
|
||||
SessionCommand::OpenHome(_) => {
|
||||
cancel_pending_output_transition_for_explicit_target(
|
||||
context.session,
|
||||
"Return to the home session",
|
||||
);
|
||||
self.save_current_or_continue(context)
|
||||
}
|
||||
SessionCommand::Clear => {
|
||||
self.capture_input_generation(context);
|
||||
self.work(
|
||||
@@ -165,6 +172,27 @@ impl ExplicitSessionTransaction {
|
||||
)))
|
||||
}
|
||||
|
||||
/// Home is applied by the runtime, which loads it like a launch would; the
|
||||
/// transaction only hands over what the load found.
|
||||
pub(super) fn complete_load_home(
|
||||
&mut self,
|
||||
_context: &mut SessionTransaction<'_>,
|
||||
outcome: Option<PersistenceOutcome>,
|
||||
) -> Result<TransactionStep> {
|
||||
let PersistenceOutcome::Load(load) = required_outcome(outcome)? else {
|
||||
return Err(anyhow!("unexpected home-session load outcome"));
|
||||
};
|
||||
let SessionCommand::OpenHome(Some(options)) = &self.command else {
|
||||
unreachable!()
|
||||
};
|
||||
Ok(TransactionStep::Complete(Box::new(
|
||||
SessionCommandReport::Home(RuntimeHomeSessionReport {
|
||||
options: Some((**options).clone()),
|
||||
outcome: Some(load),
|
||||
}),
|
||||
)))
|
||||
}
|
||||
|
||||
pub(super) fn complete_save_as_preflight(
|
||||
&mut self,
|
||||
context: &mut SessionTransaction<'_>,
|
||||
|
||||
@@ -64,7 +64,7 @@ impl WaylandState {
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn handle_session_load_outcome_for_options(
|
||||
pub(in crate::backend::wayland::state) fn handle_session_load_outcome_for_options(
|
||||
&mut self,
|
||||
outcome: session::LoadSnapshotOutcome,
|
||||
options: &session::SessionOptions,
|
||||
|
||||
@@ -65,7 +65,7 @@ impl WaylandState {
|
||||
/// switch stands.
|
||||
pub(in crate::backend::wayland) fn session_target_committed(&mut self) {
|
||||
let target = session_target(self.session.options());
|
||||
let Some(session) = self.session_home.report_for(target) else {
|
||||
let Some(session) = self.session_home.commit_target(target) else {
|
||||
return;
|
||||
};
|
||||
if let Err(error) = publish_session_from_environment(&session) {
|
||||
|
||||
@@ -110,7 +110,7 @@ impl WaylandState {
|
||||
// toolbar snapshot is built between canvas renders, and before the
|
||||
// first one, so a published value would lag or not exist yet.
|
||||
snapshot.spotlight_magnifier_source = Some(self.current_spotlight_magnifier_source());
|
||||
populate_session_snapshot(&mut snapshot, self.session.options());
|
||||
populate_session_snapshot(&mut snapshot, self.session.options(), &self.session_home);
|
||||
snapshot.runtime_ui_persistence = self
|
||||
.preferences
|
||||
.runtime_ui()
|
||||
|
||||
@@ -10,7 +10,10 @@ use wayland_client::{Connection, QueueHandle};
|
||||
pub(super) fn populate_session_snapshot(
|
||||
snapshot: &mut ToolbarSnapshot,
|
||||
options: Option<&crate::session::SessionOptions>,
|
||||
home: &crate::backend::wayland::session::SessionHome,
|
||||
) {
|
||||
snapshot.home_session_name = home.name();
|
||||
snapshot.at_home_session = home.is_at_home();
|
||||
let active_path = options.map(|options| options.session_file_path());
|
||||
snapshot.active_session_name = active_path.as_deref().map(session_display_name);
|
||||
// Recents are only read (from the catalog on disk) while the top strip's
|
||||
@@ -104,6 +107,7 @@ fn recent_session_snapshots(
|
||||
}
|
||||
|
||||
mod dialog;
|
||||
mod home;
|
||||
|
||||
pub(in crate::backend::wayland::state) use dialog::SessionFileDialogController;
|
||||
pub(super) use dialog::{SessionFileDialogMode, ensure_save_as_extension};
|
||||
@@ -128,6 +132,10 @@ impl WaylandState {
|
||||
self.handle_toolbar_open_session_path(path);
|
||||
true
|
||||
}
|
||||
ToolbarEvent::OpenHomeSession => {
|
||||
self.handle_toolbar_open_home_session();
|
||||
true
|
||||
}
|
||||
ToolbarEvent::SaveSessionAs => {
|
||||
self.handle_toolbar_save_session_as(conn, qh);
|
||||
true
|
||||
@@ -419,6 +427,7 @@ impl WaylandState {
|
||||
self.set_session_toolbar_info(format!("Opened session {name}"));
|
||||
}
|
||||
}
|
||||
SessionCommandReport::Home(report) => self.finish_open_home_session(report),
|
||||
SessionCommandReport::SaveAs(report) => {
|
||||
self.clear_toolbar_save_as_overwrite_prompt();
|
||||
self.set_session_toolbar_info(format!(
|
||||
@@ -503,6 +512,7 @@ impl WaylandState {
|
||||
}
|
||||
let prefix = match command {
|
||||
SessionCommand::Open(_) => "Open session failed",
|
||||
SessionCommand::OpenHome(_) => "Return to the home session failed",
|
||||
SessionCommand::SaveAs(..) | SessionCommand::CheckOverwrite(_) => "Save session failed",
|
||||
SessionCommand::Clear => "Clear session failed",
|
||||
SessionCommand::ClearTools(_) => "Failed to reset tool defaults",
|
||||
@@ -612,8 +622,34 @@ fn dialog_frame_accepted(phase: DialogFramePhase, outcome: RenderOutcome) -> boo
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{DialogFramePhase, dialog_frame_accepted};
|
||||
use super::{DialogFramePhase, dialog_frame_accepted, populate_session_snapshot};
|
||||
use crate::backend::wayland::session::{SessionHome, SessionLaunch};
|
||||
use crate::backend::wayland::state::RenderOutcome;
|
||||
use crate::backend::wayland::state::toolbar::ToolbarSnapshot;
|
||||
use crate::session::SessionTarget;
|
||||
|
||||
#[test]
|
||||
fn the_session_menu_learns_home_and_whether_it_is_active() {
|
||||
let input = crate::input::state::test_support::make_test_input_state();
|
||||
let mut snapshot = ToolbarSnapshot::from_input_with_bindings(&input, Default::default());
|
||||
let home = std::path::PathBuf::from("/sessions/home.wayscriber-session");
|
||||
let away = SessionHome::new(
|
||||
SessionLaunch {
|
||||
home: crate::backend::wayland::session::HomeSession::Named(home),
|
||||
preferred: None,
|
||||
},
|
||||
None,
|
||||
SessionTarget::NamedFile("/sessions/b.wayscriber-session".into()),
|
||||
);
|
||||
|
||||
populate_session_snapshot(&mut snapshot, None, &away);
|
||||
|
||||
assert_eq!(
|
||||
snapshot.home_session_name.as_deref(),
|
||||
Some("home.wayscriber-session")
|
||||
);
|
||||
assert!(!snapshot.at_home_session);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dialog_entry_requires_a_committed_frame() {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
use super::*;
|
||||
use crate::backend::wayland::session::RuntimeHomeSessionReport;
|
||||
use crate::session::{SessionOptions, SessionSnapshot};
|
||||
|
||||
impl WaylandState {
|
||||
/// Returns to the home session the way Open switches session: the current
|
||||
/// session is saved first, and nothing changes unless home loads.
|
||||
pub(super) fn handle_toolbar_open_home_session(&mut self) {
|
||||
self.clear_toolbar_save_as_overwrite_prompt();
|
||||
let command = SessionCommand::OpenHome(self.home_session_options().map(Box::new));
|
||||
if let Err(error) = self.start_session_command(command) {
|
||||
self.report_session_command_error("Return to the home session failed", &error);
|
||||
}
|
||||
}
|
||||
|
||||
/// Home's options for the output the overlay is on now, not those of the
|
||||
/// named session it is leaving.
|
||||
fn home_session_options(&self) -> Option<SessionOptions> {
|
||||
let mut options = self.session_home.options()?.clone();
|
||||
let output_identity = self
|
||||
.surface
|
||||
.current_output()
|
||||
.as_ref()
|
||||
.and_then(|output| self.output_identity_for(output));
|
||||
options.set_output_identity(output_identity.as_deref());
|
||||
Some(options)
|
||||
}
|
||||
|
||||
pub(super) fn finish_open_home_session(&mut self, report: RuntimeHomeSessionReport) {
|
||||
let applied = match (report.options, report.outcome) {
|
||||
(Some(options), Some(outcome)) => {
|
||||
let loaded_board_data = outcome.has_board_data();
|
||||
self.handle_session_load_outcome_for_options(outcome, &options, "home session")
|
||||
.map(|()| {
|
||||
self.input_state
|
||||
.set_session_preflight_options(Some(options.clone()));
|
||||
self.session
|
||||
.commit_output_options(options, loaded_board_data);
|
||||
})
|
||||
}
|
||||
_ => self.leave_persistence_for_home(),
|
||||
};
|
||||
if let Err(error) = applied {
|
||||
self.report_session_command_error("Return to the home session failed", &error);
|
||||
return;
|
||||
}
|
||||
|
||||
self.session_target_committed();
|
||||
self.set_session_toolbar_info(format!("Returned to {}", self.session_home.label()));
|
||||
}
|
||||
|
||||
/// Home has persistence disabled: the run continues on an empty canvas
|
||||
/// that is not saved, as it would have started.
|
||||
fn leave_persistence_for_home(&mut self) -> Result<()> {
|
||||
let current = self
|
||||
.session_options()
|
||||
.cloned()
|
||||
.context("no session to return home from")?;
|
||||
let empty = SessionSnapshot {
|
||||
active_board_id: self.input_state.board_id().to_string(),
|
||||
boards: Vec::new(),
|
||||
tool_state: None,
|
||||
};
|
||||
crate::session::apply_snapshot_replacing_boards(
|
||||
&mut self.input_state,
|
||||
self.render.text_measurer(),
|
||||
empty,
|
||||
¤t,
|
||||
)?;
|
||||
self.input_state.set_session_preflight_options(None);
|
||||
self.input_state.clear_session_dirty();
|
||||
self.session.commit_without_persistence();
|
||||
self.refresh_runtime_ui_config_seeds();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -210,6 +210,7 @@ fn runtime_toolbar_events_do_not_directly_save_config() {
|
||||
ToolbarEvent::ApplyPreset(1),
|
||||
ToolbarEvent::OpenSession,
|
||||
ToolbarEvent::OpenRecentSession(std::path::PathBuf::from("/tmp/recent.wayscriber-session")),
|
||||
ToolbarEvent::OpenHomeSession,
|
||||
ToolbarEvent::SaveSessionAs,
|
||||
ToolbarEvent::SaveSessionAsConfirm(std::path::PathBuf::from(
|
||||
"/tmp/existing.wayscriber-session",
|
||||
@@ -1023,6 +1024,7 @@ fn session_popover_survives_its_own_controls_and_dismisses_on_everything_else()
|
||||
for spared in [
|
||||
ToolbarEvent::OpenSession,
|
||||
ToolbarEvent::OpenRecentSession(PathBuf::from("/tmp/recent.wayscriber-session")),
|
||||
ToolbarEvent::OpenHomeSession,
|
||||
ToolbarEvent::SaveSessionAs,
|
||||
ToolbarEvent::SaveSessionAsConfirm(PathBuf::from("/tmp/existing.wayscriber-session")),
|
||||
ToolbarEvent::SaveSessionAsCancel,
|
||||
|
||||
@@ -362,6 +362,23 @@ fn session_menu_content(snapshot: &ToolbarSnapshot) -> Option<Vec<WidgetNode>> {
|
||||
y += grid.height;
|
||||
}
|
||||
|
||||
if let Some(home) = model.home.as_ref() {
|
||||
y += MENU_GAP;
|
||||
nodes.push(text_button(
|
||||
"top.menu.session.home".to_owned(),
|
||||
(0.0, y, MENU_CONTENT_W, MENU_BUTTON_H),
|
||||
LabelSpec::new(truncate_middle(&home.label, 30), MENU_LABEL_FONT, true),
|
||||
if home.enabled {
|
||||
ButtonStyle::plain()
|
||||
} else {
|
||||
ButtonStyle::disabled()
|
||||
},
|
||||
home.enabled
|
||||
.then(|| Interaction::click(home.event(), Some(home.label.clone()))),
|
||||
));
|
||||
y += MENU_BUTTON_H;
|
||||
}
|
||||
|
||||
for (index, recent) in model.recents.iter().enumerate() {
|
||||
y += MENU_GAP;
|
||||
let tooltip_path = recent.path.display().to_string();
|
||||
|
||||
@@ -1931,6 +1931,13 @@ fn session_popover_re_hosts_the_session_pane_content() {
|
||||
assert_session_nodes_inside_panel(&tree);
|
||||
assert_session_popover_input_rect(&snapshot, &tree, w, h);
|
||||
|
||||
// Away from home, the way back is offered.
|
||||
snapshot.home_session_name = Some("home.wayscriber-session".to_string());
|
||||
snapshot.at_home_session = false;
|
||||
let tree = build(&snapshot);
|
||||
assert_session_popover_model(&tree, &snapshot);
|
||||
assert_session_nodes_inside_panel(&tree);
|
||||
|
||||
// A pending Save-As overwrite swaps the button grid for the
|
||||
// confirmation, exactly like the pane.
|
||||
snapshot.pending_save_as_overwrite_path =
|
||||
@@ -1974,6 +1981,24 @@ fn assert_session_popover_model(tree: &WidgetTree, snapshot: &ToolbarSnapshot) {
|
||||
.expect("recent row");
|
||||
assert_eq!(node.interact.as_ref().unwrap().event, recent.event());
|
||||
}
|
||||
let home = model.home.as_ref().expect("home row");
|
||||
let node = tree
|
||||
.node_by_id(&"top.menu.session.home".into())
|
||||
.expect("home row");
|
||||
match &node.kind {
|
||||
WidgetKind::TextButton { label, style } => {
|
||||
assert_eq!(
|
||||
label.text,
|
||||
crate::ui::toolbar::session_format::truncate_middle(&home.label, 30)
|
||||
);
|
||||
assert_eq!(style.disabled, !home.enabled);
|
||||
}
|
||||
other => panic!("home row kind, got {other:?}"),
|
||||
}
|
||||
assert_eq!(
|
||||
node.interact.as_ref().map(|interact| &interact.event),
|
||||
home.enabled.then(|| home.event()).as_ref()
|
||||
);
|
||||
// Meta labels are decor; this popover has no collapsible header.
|
||||
assert!(tree.node_by_id(&"top.menu.session.name".into()).is_some());
|
||||
assert!(tree.node_by_id(&"top.menu.session.path".into()).is_some());
|
||||
|
||||
@@ -126,6 +126,7 @@ pub const SIDE_SETTINGS_ABOUT: ToolbarItemId = ToolbarItemId::from_known("side.s
|
||||
|
||||
pub const SIDE_SESSION_OPEN: ToolbarItemId = ToolbarItemId::from_known("side.session.open");
|
||||
pub const SIDE_SESSION_SAVE_AS: ToolbarItemId = ToolbarItemId::from_known("side.session.save-as");
|
||||
pub const SIDE_SESSION_HOME: ToolbarItemId = ToolbarItemId::from_known("side.session.home");
|
||||
pub const SIDE_SESSION_INFO: ToolbarItemId = ToolbarItemId::from_known("side.session.info");
|
||||
pub const SIDE_SESSION_CLEAR: ToolbarItemId = ToolbarItemId::from_known("side.session.clear");
|
||||
pub const SIDE_SESSION_MANAGER: ToolbarItemId = ToolbarItemId::from_known("side.session.manager");
|
||||
@@ -452,6 +452,13 @@ const TOOLBAR_ITEM_DEFINITIONS: &[ToolbarItemDefinition] = &[
|
||||
Session,
|
||||
Some(ToolbarGroupId::Session),
|
||||
),
|
||||
item(
|
||||
ids::SIDE_SESSION_HOME,
|
||||
"Return to home session",
|
||||
Popover,
|
||||
Session,
|
||||
Some(ToolbarGroupId::Session),
|
||||
),
|
||||
item(
|
||||
ids::SIDE_SESSION_INFO,
|
||||
"Session info",
|
||||
|
||||
@@ -362,6 +362,7 @@ impl InputState {
|
||||
ToolbarEvent::ClearPreset(slot) => self.apply_toolbar_clear_preset(slot),
|
||||
ToolbarEvent::OpenSession
|
||||
| ToolbarEvent::OpenRecentSession(_)
|
||||
| ToolbarEvent::OpenHomeSession
|
||||
| ToolbarEvent::SaveSessionAs
|
||||
| ToolbarEvent::SaveSessionAsConfirm(_)
|
||||
| ToolbarEvent::SaveSessionAsCancel
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Session pane: active-session meta labels, the Open / Save As / Info /
|
||||
//! Clear / Manager grid (replaced by the Save-As overwrite confirmation
|
||||
//! while one is pending), and the recent-session list.
|
||||
//! while one is pending), the way back to the home session, and the
|
||||
//! recent-session list.
|
||||
|
||||
use gtk4::prelude::*;
|
||||
|
||||
@@ -54,6 +55,9 @@ pub(in crate::toolbar_gtk) fn build_popover_content(
|
||||
column.append(&grid);
|
||||
}
|
||||
|
||||
if let Some(home) = session.home.as_ref() {
|
||||
column.append(&home_button(ctx, home));
|
||||
}
|
||||
for recent in &session.recents {
|
||||
column.append(&recent_row(ctx, recent));
|
||||
}
|
||||
@@ -131,6 +135,22 @@ fn overwrite_confirmation_rows(
|
||||
rows
|
||||
}
|
||||
|
||||
fn home_button(ctx: &SectionCtx, home: &model::session::ToolbarSessionHome) -> gtk4::Button {
|
||||
let button = gtk4::Button::new();
|
||||
button.set_size_request(-1, ctx.px(24.0));
|
||||
let label = gtk4::Label::new(Some(&home.label));
|
||||
label.set_ellipsize(gtk4::pango::EllipsizeMode::Middle);
|
||||
button.set_child(Some(&label));
|
||||
button.set_tooltip_text(Some(&home.label));
|
||||
button.set_sensitive(home.enabled);
|
||||
let sender = ctx.feedback.clone();
|
||||
let event = home.event();
|
||||
button.connect_clicked(move |_| {
|
||||
send_event(&sender, event.clone());
|
||||
});
|
||||
button
|
||||
}
|
||||
|
||||
fn recent_row(ctx: &SectionCtx, recent: &model::ToolbarSessionRecent) -> gtk4::Button {
|
||||
let button = gtk4::Button::new();
|
||||
button.set_size_request(-1, ctx.px(22.0));
|
||||
|
||||
@@ -222,6 +222,8 @@ struct SessionMenuContentKey {
|
||||
active_session_name: Option<String>,
|
||||
active_session_path: Option<std::path::PathBuf>,
|
||||
recent_sessions: Vec<crate::ui::toolbar::SessionRecentSnapshot>,
|
||||
home_session_name: Option<String>,
|
||||
at_home_session: bool,
|
||||
pending_save_as_overwrite_path: Option<std::path::PathBuf>,
|
||||
use_icons: bool,
|
||||
}
|
||||
@@ -233,6 +235,8 @@ impl SessionMenuContentKey {
|
||||
active_session_name: snapshot.active_session_name.clone(),
|
||||
active_session_path: snapshot.active_session_path.clone(),
|
||||
recent_sessions: snapshot.recent_sessions.clone(),
|
||||
home_session_name: snapshot.home_session_name.clone(),
|
||||
at_home_session: snapshot.at_home_session,
|
||||
pending_save_as_overwrite_path: snapshot.pending_save_as_overwrite_path.clone(),
|
||||
use_icons: snapshot.use_icons,
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ pub(super) fn assert_menu_popover_contracts(regular: &ToolbarSnapshot) {
|
||||
display_name: "recent-0.wayscriber-session".to_string(),
|
||||
path: std::path::PathBuf::from("/tmp/recent-0.wayscriber-session"),
|
||||
}];
|
||||
session_snapshot.home_session_name = Some("home.wayscriber-session".to_string());
|
||||
session_snapshot.at_home_session = false;
|
||||
let (tx, menu_rx) = std::sync::mpsc::channel();
|
||||
let mut menu_top = TopBar::new_for_test(FeedbackSender::new(tx));
|
||||
// Building the strip creates the two overflow-anchored native popovers.
|
||||
@@ -52,9 +54,10 @@ fn assert_session_popover_contract(
|
||||
find_widget_named(&content, "top.menu.session.panel").expect("session popover panel box");
|
||||
let mut buttons: Vec<gtk4::Button> = Vec::new();
|
||||
collect_descendants(&panel, &mut buttons);
|
||||
let home = model.home.as_ref().expect("home row");
|
||||
assert_eq!(
|
||||
buttons.len(),
|
||||
model.buttons.len() + model.recents.len(),
|
||||
model.buttons.len() + 1 + model.recents.len(),
|
||||
"the popover exposes exactly the pane's controls"
|
||||
);
|
||||
for (button, button_model) in buttons.iter().zip(model.buttons.iter()) {
|
||||
@@ -70,6 +73,21 @@ fn assert_session_popover_contract(
|
||||
rebind_requested: false,
|
||||
}
|
||||
);
|
||||
let home_button = &buttons[model.buttons.len()];
|
||||
assert_eq!(
|
||||
home_button.tooltip_text().as_deref(),
|
||||
Some(home.label.as_str())
|
||||
);
|
||||
assert_eq!(home_button.is_sensitive(), home.enabled);
|
||||
home_button.emit_clicked();
|
||||
assert_eq!(
|
||||
rx.recv_timeout(Duration::from_secs(1))
|
||||
.expect("GTK home event"),
|
||||
GtkToolbarFeedback::Event {
|
||||
event: home.event(),
|
||||
rebind_requested: false,
|
||||
}
|
||||
);
|
||||
buttons.last().expect("recent row button").emit_clicked();
|
||||
assert_eq!(
|
||||
rx.recv_timeout(Duration::from_secs(1))
|
||||
|
||||
@@ -166,6 +166,9 @@ pub enum ToolbarEvent {
|
||||
ClearPreset(usize),
|
||||
OpenSession,
|
||||
OpenRecentSession(PathBuf),
|
||||
/// Return to the home session: the daemon's startup session file, or the
|
||||
/// configured default session.
|
||||
OpenHomeSession,
|
||||
SaveSessionAs,
|
||||
SaveSessionAsConfirm(PathBuf),
|
||||
SaveSessionAsCancel,
|
||||
|
||||
@@ -236,6 +236,7 @@ pub(crate) fn action_for_event(event: &ToolbarEvent) -> Option<Action> {
|
||||
// and Settings preferences: toolbar state rather than drawing actions.
|
||||
ToolbarEvent::OpenSession
|
||||
| ToolbarEvent::OpenRecentSession(_)
|
||||
| ToolbarEvent::OpenHomeSession
|
||||
| ToolbarEvent::SaveSessionAs
|
||||
| ToolbarEvent::SaveSessionAsConfirm(_)
|
||||
| ToolbarEvent::SaveSessionAsCancel
|
||||
@@ -464,6 +465,7 @@ pub(crate) fn popovers_for_event(event: &ToolbarEvent) -> &'static [ToolbarPopov
|
||||
// Session hosts the session controls.
|
||||
ToolbarEvent::OpenSession
|
||||
| ToolbarEvent::OpenRecentSession(_)
|
||||
| ToolbarEvent::OpenHomeSession
|
||||
| ToolbarEvent::SaveSessionAs
|
||||
| ToolbarEvent::SaveSessionAsConfirm(_)
|
||||
| ToolbarEvent::SaveSessionAsCancel
|
||||
@@ -747,6 +749,7 @@ fn persistence_for_event(event: &ToolbarEvent) -> ToolbarPersistence {
|
||||
| ToolbarEvent::ClearPreset(_)
|
||||
| ToolbarEvent::OpenSession
|
||||
| ToolbarEvent::OpenRecentSession(_)
|
||||
| ToolbarEvent::OpenHomeSession
|
||||
| ToolbarEvent::SaveSessionAs
|
||||
| ToolbarEvent::SaveSessionAsConfirm(_)
|
||||
| ToolbarEvent::SaveSessionAsCancel
|
||||
|
||||
@@ -684,6 +684,20 @@ mod tests {
|
||||
.any(|button| button.event == ToolbarEvent::SessionInfo),
|
||||
"hiding side.session.info removes the Session Info control"
|
||||
);
|
||||
assert!(
|
||||
ToolbarSessionModel::for_popover(&snapshot)
|
||||
.expect("session")
|
||||
.home
|
||||
.is_some()
|
||||
);
|
||||
hide(&mut snapshot, ids::SIDE_SESSION_HOME);
|
||||
assert!(
|
||||
ToolbarSessionModel::for_popover(&snapshot)
|
||||
.expect("session")
|
||||
.home
|
||||
.is_none(),
|
||||
"hiding side.session.home removes the way back to the home session"
|
||||
);
|
||||
|
||||
snapshot = self::snapshot();
|
||||
let about_visible = |snapshot: &ToolbarSnapshot| {
|
||||
|
||||
@@ -12,6 +12,7 @@ pub(crate) struct ToolbarSessionModel {
|
||||
pub(crate) active_name: String,
|
||||
pub(crate) active_path_label: String,
|
||||
pub(crate) buttons: Vec<ToolbarSessionButton>,
|
||||
pub(crate) home: Option<ToolbarSessionHome>,
|
||||
pub(crate) recents: Vec<ToolbarSessionRecent>,
|
||||
pub(crate) overwrite_confirmation: Option<ToolbarSessionOverwriteConfirmation>,
|
||||
}
|
||||
@@ -44,6 +45,8 @@ impl ToolbarSessionModel {
|
||||
.into_iter()
|
||||
.filter(|button| session_button_visible(snapshot, &button.event))
|
||||
.collect();
|
||||
let home = (!snapshot.toolbar_item_hidden(ids::SIDE_SESSION_HOME))
|
||||
.then(|| ToolbarSessionHome::from_snapshot(snapshot));
|
||||
let recents = if target_active {
|
||||
snapshot
|
||||
.recent_sessions
|
||||
@@ -62,10 +65,11 @@ impl ToolbarSessionModel {
|
||||
path: path.clone(),
|
||||
});
|
||||
|
||||
(!buttons.is_empty() || !recents.is_empty()).then_some(Self {
|
||||
(!buttons.is_empty() || home.is_some() || !recents.is_empty()).then_some(Self {
|
||||
active_name,
|
||||
active_path_label,
|
||||
buttons,
|
||||
home,
|
||||
recents,
|
||||
overwrite_confirmation,
|
||||
})
|
||||
@@ -109,6 +113,32 @@ impl ToolbarSessionButton {
|
||||
}
|
||||
}
|
||||
|
||||
/// The way back to the home session: the daemon's startup session file, or
|
||||
/// the configured default session.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ToolbarSessionHome {
|
||||
/// "Back to <session>", or "Default session".
|
||||
pub(crate) label: String,
|
||||
/// Off while home is already active: there is nothing to return to.
|
||||
pub(crate) enabled: bool,
|
||||
}
|
||||
|
||||
impl ToolbarSessionHome {
|
||||
fn from_snapshot(snapshot: &ToolbarSnapshot) -> Self {
|
||||
Self {
|
||||
label: snapshot.home_session_name.as_ref().map_or_else(
|
||||
|| "Default session".to_owned(),
|
||||
|name| format!("Back to {name}"),
|
||||
),
|
||||
enabled: !snapshot.at_home_session,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn event(&self) -> ToolbarEvent {
|
||||
ToolbarEvent::OpenHomeSession
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ToolbarSessionRecent {
|
||||
pub(crate) label: String,
|
||||
@@ -220,6 +250,32 @@ mod tests {
|
||||
assert_eq!(model.active_path_label, "No persisted session target");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_model_offers_the_way_home_only_away_from_home() {
|
||||
let mut snapshot = app_snapshot();
|
||||
snapshot.active_session_path = Some(PathBuf::from("/tmp/b.wayscriber-session"));
|
||||
|
||||
for (name, label) in [
|
||||
(None, "Default session"),
|
||||
(
|
||||
Some("home.wayscriber-session"),
|
||||
"Back to home.wayscriber-session",
|
||||
),
|
||||
] {
|
||||
snapshot.home_session_name = name.map(str::to_owned);
|
||||
for at_home in [false, true] {
|
||||
snapshot.at_home_session = at_home;
|
||||
|
||||
let model = ToolbarSessionModel::for_popover(&snapshot).expect("session model");
|
||||
let home = model.home.expect("home row");
|
||||
|
||||
assert_eq!(home.label, label);
|
||||
assert_eq!(home.enabled, !at_home, "{label}, at home {at_home}");
|
||||
assert_eq!(home.event(), ToolbarEvent::OpenHomeSession);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_model_exposes_pending_save_as_overwrite_confirmation() {
|
||||
let mut snapshot = app_snapshot();
|
||||
|
||||
@@ -241,6 +241,8 @@ impl ToolbarSnapshot {
|
||||
active_session_name: None,
|
||||
active_session_path: None,
|
||||
recent_sessions: Vec::new(),
|
||||
home_session_name: None,
|
||||
at_home_session: true,
|
||||
pending_save_as_overwrite_path: state.pending_save_as_overwrite().map(PathBuf::from),
|
||||
runtime_ui_persistence: None,
|
||||
}
|
||||
|
||||
@@ -465,6 +465,11 @@ pub struct ToolbarSnapshot {
|
||||
pub active_session_path: Option<PathBuf>,
|
||||
/// Recent persisted sessions from the catalog.
|
||||
pub recent_sessions: Vec<SessionRecentSnapshot>,
|
||||
/// The named session the Session menu returns home to; `None` is the
|
||||
/// configured default session.
|
||||
pub home_session_name: Option<String>,
|
||||
/// Whether the active session is home, leaving nothing to return to.
|
||||
pub at_home_session: bool,
|
||||
/// Save Session As target waiting for explicit overwrite confirmation.
|
||||
pub pending_save_as_overwrite_path: Option<PathBuf>,
|
||||
/// Generated runtime UI preference persistence and recovery state.
|
||||
|
||||
Reference in new issue
Block a user