Skip to content
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
39 changes: 38 additions & 1 deletion crates/hidegit-ui/src/widget/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down
16 changes: 2 additions & 14 deletions crates/hidegit-ui/src/widget/detail.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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> {
Expand Down
29 changes: 9 additions & 20 deletions crates/hidegit-ui/src/widget/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -103,16 +104,16 @@ pub fn view<'a>(
staging: Option<Staging<'a>>,
) -> 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)
Expand All @@ -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()),
Expand All @@ -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),
Expand Down Expand Up @@ -660,18 +661,6 @@ fn gutter<'a>(lineno: Option<u32>, 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::*;
Expand Down
14 changes: 3 additions & 11 deletions crates/hidegit-ui/src/widget/staging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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| {
Expand All @@ -159,15 +159,15 @@ 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.
Section::Conflicted => match resolver {
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),
},
}
}
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions docs/UI_SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading