diff --git a/crates/hidegit-ui/src/widget/common.rs b/crates/hidegit-ui/src/widget/common.rs index 5a0aa4b..65c0f93 100644 --- a/crates/hidegit-ui/src/widget/common.rs +++ b/crates/hidegit-ui/src/widget/common.rs @@ -19,7 +19,7 @@ //! a real obstacle and is not: a rule emits nothing, so the message type is //! free and these are generic over it. -use iced::widget::{Space, container}; +use iced::widget::{Space, container, text}; use iced::{Fill, Length}; use crate::Element; @@ -51,6 +51,42 @@ fn rule<'a, M: 'a>(palette: &Palette, width: Length, height: Length) -> Element< .into() } +/// What a pane shows when it holds nothing yet. +/// +/// Three of these existed — `empty` in the diff, `placeholder` in the detail +/// pane and a third `placeholder` in staging — rendering the same centred muted +/// line three ways. +/// +/// `UI_SPEC.md` asks empty states to "carry the next action, not just an +/// absence", and most of these do not yet. Giving them one is a separate change; +/// what this does is make there be one place to give it to. +pub fn empty<'a, M: 'a>(message: impl text::IntoFragment<'a>, palette: &Palette) -> Element<'a, M> { + centred(message, palette) +} + +/// What a pane shows while it is still reading. +/// +/// The same picture as [`empty`] and deliberately not the same function. A pane +/// with nothing in it and a pane that has not finished looking are different +/// states, and the difference is about to matter: an empty state is getting the +/// next action, and "Loading…" has no next action to offer. Two of the seven +/// call sites this replaced were this state wearing the other one's name. +pub fn loading<'a, M: 'a>( + message: impl text::IntoFragment<'a>, + palette: &Palette, +) -> Element<'a, M> { + centred(message, palette) +} + +fn centred<'a, M: 'a>(message: impl text::IntoFragment<'a>, palette: &Palette) -> Element<'a, M> { + let muted = palette.muted; + container(text(message).size(metrics::text::BODY).color(muted)) + .width(Fill) + .height(Fill) + .center(Fill) + .into() +} + /// How a button is drawn, by what pressing it does. /// /// Four designs for "the primary button" existed at once and disagreed about @@ -202,6 +238,7 @@ mod tests { "fn divider", "fn vertical_rule", "fn horizontal_rule", + "fn placeholder", "fn accent_style", "fn danger_style", "fn quiet_style", diff --git a/crates/hidegit-ui/src/widget/detail.rs b/crates/hidegit-ui/src/widget/detail.rs index 59d71c1..8d6aa0d 100644 --- a/crates/hidegit-ui/src/widget/detail.rs +++ b/crates/hidegit-ui/src/widget/detail.rs @@ -14,8 +14,8 @@ use crate::widget::common; pub fn view<'a>(repo: &'a OpenRepo, palette: &'a Palette) -> Element<'a, RepoMessage> { let body: Element<'a, RepoMessage> = match &repo.detail { - DetailPane::Empty => placeholder("Select a commit to read its message and diff", palette), - DetailPane::Loading => placeholder("Loading…", palette), + DetailPane::Empty => common::empty("Select a commit to read its message and diff", palette), + DetailPane::Loading => common::loading("Loading…", palette), DetailPane::Failed(error) => failure(error, palette), DetailPane::Commit { detail, diff, file } => { // A stash *is* a commit, which is what lets it reuse all of this — but @@ -413,18 +413,6 @@ fn commit<'a>( .into() } -fn placeholder<'a>(message: &str, palette: &Palette) -> Element<'a, RepoMessage> { - container( - text(message.to_owned()) - .size(metrics::text::BODY) - .color(palette.muted), - ) - .width(Fill) - .height(Fill) - .center(Fill) - .into() -} - /// A failure shown where the action was attempted, with Git's own words rather /// than a paraphrase. fn failure<'a>(error: &UiError, palette: &Palette) -> Element<'a, RepoMessage> { diff --git a/crates/hidegit-ui/src/widget/diff.rs b/crates/hidegit-ui/src/widget/diff.rs index bc44b0e..66afbbf 100644 --- a/crates/hidegit-ui/src/widget/diff.rs +++ b/crates/hidegit-ui/src/widget/diff.rs @@ -17,6 +17,7 @@ use crate::message::RepoMessage; use crate::metrics; use crate::state::DiffMode; use crate::theme::Palette; +use crate::widget::common; const GUTTER_WIDTH: f32 = 44.0; @@ -103,16 +104,16 @@ pub fn view<'a>( staging: Option>, ) -> Element<'a, RepoMessage> { let Some(file) = diff.files.get(file) else { - return empty("Select a file to see its changes", palette); + return common::empty("Select a file to see its changes", palette); }; match &file.content { - FileDiffContent::Binary => empty( - &format!("{} is binary — no text to show", file.path.display()), + FileDiffContent::Binary => common::empty( + format!("{} is binary — no text to show", file.path.display()), palette, ), - FileDiffContent::TooLarge { bytes } => empty( - &format!( + FileDiffContent::TooLarge { bytes } => common::empty( + format!( "{} is {} — too large to diff without stalling", file.path.display(), format::bytes(*bytes) @@ -122,8 +123,8 @@ pub fn view<'a>( // The size, not the pointer. Three lines of `oid sha256:…` are what // Git stores, not what changed — and "4.2 MB → 5.1 MB" is the most a // diff can honestly say about a file whose content it does not have. - FileDiffContent::Lfs { old, new, fetched } => empty( - &format!( + FileDiffContent::Lfs { old, new, fetched } => common::empty( + format!( "{} is stored with Git LFS — {}{}", file.path.display(), lfs_summary(old.as_ref(), new.as_ref()), @@ -132,7 +133,7 @@ pub fn view<'a>( palette, ), FileDiffContent::Text { hunks } if hunks.is_empty() => { - empty("No textual changes — the file's mode changed", palette) + common::empty("No textual changes — the file's mode changed", palette) } FileDiffContent::Text { hunks } => match mode { DiffMode::Unified => unified(file, hunks, palette, staging), @@ -660,18 +661,6 @@ fn gutter<'a>(lineno: Option, palette: &Palette) -> Element<'a, RepoMessage .into() } -fn empty<'a>(message: &str, palette: &Palette) -> Element<'a, RepoMessage> { - container( - text(message.to_owned()) - .size(metrics::text::BODY) - .color(palette.muted), - ) - .width(Fill) - .height(Fill) - .center(Fill) - .into() -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/hidegit-ui/src/widget/staging.rs b/crates/hidegit-ui/src/widget/staging.rs index 73c4616..7d9df90 100644 --- a/crates/hidegit-ui/src/widget/staging.rs +++ b/crates/hidegit-ui/src/widget/staging.rs @@ -143,7 +143,7 @@ fn pane<'a>( palette: &'a Palette, ) -> Element<'a, RepoMessage> { let Some(row) = selected else { - return placeholder("Select a file to see its changes", palette); + return common::empty("Select a file to see its changes", palette); }; let staging = |is_staged: bool| { @@ -159,7 +159,7 @@ fn pane<'a>( Section::Unstaged => diff::view(unstaged, row.index, mode, palette, staging(false)), // An untracked file has no diff to show: nothing in the repository has // ever seen it, so every line would be an addition against nothing. - Section::Untracked => placeholder("Untracked — stage it to see it as a diff", palette), + Section::Untracked => common::empty("Untracked — stage it to see it as a diff", palette), // The resolver loads its file asynchronously, so between selecting the // row and the file arriving there is genuinely nothing to show. Saying // so beats an empty pane that looks broken. @@ -167,7 +167,7 @@ fn pane<'a>( Some(resolver) => { crate::widget::resolver::view(resolver, state, conflicted_paths, palette) } - None => placeholder("Reading the conflicted file…", palette), + None => common::loading("Reading the conflicted file…", palette), }, } } @@ -403,14 +403,6 @@ fn clean<'a>(palette: &Palette) -> Element<'a, RepoMessage> { .into() } -fn placeholder<'a>(message: &'a str, palette: &Palette) -> Element<'a, RepoMessage> { - let muted = palette.muted; - container(text(message).size(metrics::text::BODY).color(muted)) - .center_x(Fill) - .center_y(Fill) - .into() -} - /// The commit message editor, and the button that acts on it. /// /// Sits under the file lists rather than in the diff pane, because it is about diff --git a/docs/UI_SPEC.md b/docs/UI_SPEC.md index 745bd1d..1d75a34 100644 --- a/docs/UI_SPEC.md +++ b/docs/UI_SPEC.md @@ -932,6 +932,15 @@ on screen and taken nowhere. Copying does not dismiss the toast. PRs → connect GitHub, or "you have no open pull requests"; clean working directory → the last commit. +They are drawn by `widget/common::empty`, which replaced three helpers rendering the same centred +muted line three ways. **Most of them do not carry an action yet** — the welcome screen and the PR +panel do, and the panes that say "Select a file to see its changes" or "Nothing to commit" only +describe the absence. That gap is real and named here rather than left to be discovered. + +**A pane that is still reading is not an empty pane.** `common::loading` draws the same picture and +is deliberately a different function: an empty state is getting a next action and "Loading…" has +none to offer. Two of the seven call sites were this state wearing the other one's name. + **Drag and drop** on the graph performs merge and rebase. Drag a branch badge onto another and the action sheet opens naming both branches on every entry; nothing runs until one is chosen. The discoverability of the gesture is the point, but not at the cost of an unintended rebase.