#[cfg(test)]
mod test;
use crate::{checkbox, diff, el, theme, Theme};
use inflorescence_iced_widget::{dir_picker, nav_scrollable, report};
use inflorescence_model::model::{
IndexSet, Job, Log, ManagingRepoSubState, PickingProjectSelection,
ReadyState, RecordChanges,
};
use inflorescence_model::{action, model, selection, to_record};
use libflorescence::identity::Id;
use libflorescence::prelude::*;
use libflorescence::{file, repo, store};
use iced::widget::{
button, column, container, row, text, text_editor, text_input, Button,
Column,
};
use iced::{
alignment, font, window, Alignment, Element, Font, Length, Padding,
};
use std::borrow::Cow;
use std::cmp;
use std::path::{Path, PathBuf};
const SPACING: u32 = 10;
const DEFAULT_MIN_COL_WIDTH: usize = 512;
#[derive(Debug, Clone)]
pub enum Msg {
Action(action::FilteredMsg),
PickingRepoDir(dir_picker::Msg),
EditRecordMsg(String),
EditRecordDesc(text_editor::Action),
EditForkChannelName(String),
UnfilteredSelection(selection::UnfilteredMsg),
ToRecord(to_record::Msg),
OpenProject(PathBuf),
PickNewProject,
SelectIdentity(usize),
SubMenuPushSelectRemote(String),
SubMenuPullSelectRemote(String),
SubMenuCompareRemoteSelectRemote(String),
SubMenuCompareRemoteInputRemoteChannel(String),
}
pub fn main<'a, F>(
state: &'a model::State,
get_diff: F,
_window_id: window::Id,
) -> Element<'a, Msg, Theme>
where
F: Fn(file::IdHash) -> Option<&'a diff::File>,
{
let model::State {
window_size,
window_scale: _,
sub,
allowed_actions,
sub_menu,
report,
} = state;
if let Some(sub) = sub_menu {
match sub {
model::SubMenu::Push {
opt: Some(opt),
remote: _,
} => match opt {
model::PushOption::SelectingRemote { remote } => {
return push_selecting_remote(
state,
*window_size,
allowed_actions,
report,
remote,
sub_menu,
);
}
},
model::SubMenu::Pull {
opt: Some(opt),
remote: _,
} => match opt {
model::PullOption::SelectingRemote { remote } => {
return pull_selecting_remote(
state,
*window_size,
allowed_actions,
report,
remote,
sub_menu,
);
}
},
model::SubMenu::CompareRemote {
opt: Some(opt),
remote: _,
remote_channel: _,
} => match opt {
model::CompareRemoteOption::SelectingRemote { remote } => {
return compare_remote_selecting_remote(
state,
*window_size,
allowed_actions,
report,
remote,
sub_menu,
);
}
model::CompareRemoteOption::InputingRemoteChannel {
channel: remote_channel,
} => {
return compare_remote_input_remote_channel(
*window_size,
allowed_actions,
report,
remote_channel,
sub_menu,
);
}
},
model::SubMenu::Push { opt: None, .. }
| model::SubMenu::Pull { opt: None, .. }
| model::SubMenu::ResetChange
| model::SubMenu::InitRepo { path: _ }
| model::SubMenu::ImportFromGit { path: _ }
| model::SubMenu::Add { recursive: _ }
| model::SubMenu::CompareRemote { opt: None, .. } => {}
}
}
match sub {
model::SubState::PickingProject(state) => picking_project(
state,
*window_size,
allowed_actions,
report,
sub_menu,
),
model::SubState::PickingRepoDir(state) => picking_repo_dir(
state,
*window_size,
allowed_actions,
report,
sub_menu,
),
model::SubState::ManagingRepo(state) => managing_repo(
state,
get_diff,
*window_size,
allowed_actions,
report,
sub_menu,
),
}
}
fn picking_project<'a>(
state: &'a model::PickingProject,
window_size: iced::Size,
allowed_actions: &'a [action::Binding],
report: &'a report::Container,
sub_menu: &'a Option<model::SubMenu>,
) -> Element<'a, Msg, Theme> {
let model::PickingProject {
projects,
is_blocking,
selection,
projects_nav,
} = state;
let main = if let Some(projects) = projects {
el(column([
el(column([el(row([
el(button(text("Find or create a new project"))
.on_press(Msg::PickNewProject)
.class(
if matches!(
selection,
PickingProjectSelection::FindOrCreate
) {
theme::Button::Selected
} else {
theme::Button::Normal
},
)),
el(text("or pick a known project below")),
])
.spacing(SPACING)
.align_y(alignment::Vertical::Center))])),
el(nav_scrollable(
projects_nav,
projects.iter().enumerate().map(
|(ix, store::Project {
last_closed_time: _,
path,
})| {
el(button(text(format!("{}", path.to_string_lossy())))
.on_press_with(|| Msg::OpenProject(path.clone()))
.class(
if matches!(
selection,
PickingProjectSelection::Existing { ix: selection } if *selection == ix
) {
theme::Button::Selected
} else {
theme::Button::Normal
},
)
)
},
),
).class(if matches!(selection, PickingProjectSelection::Existing { .. }) {
theme::Scrollable::Selected
} else {
theme::Scrollable::Normal
}).width(Length::Fill).height(Length::Fill)),
])
.spacing(SPACING))
} else {
el(text(if *is_blocking {
"Waiting for a lock release on projects file..."
} else {
"Loading projects..."
}))
};
let main = el(container(main).width(Length::Fill).height(Length::Fill));
add_actions_and_report(
None,
main,
window_size,
allowed_actions,
report,
sub_menu,
false,
)
}
fn picking_repo_dir<'a>(
state: &'a model::PickingRepoDir,
window_size: iced::Size,
allowed_actions: &'a [action::Binding],
report: &'a report::Container,
sub_menu: &'a Option<model::SubMenu>,
) -> Element<'a, Msg, Theme> {
let model::PickingRepoDir {
picker,
waiting_to_init,
} = state;
let main = if let Some(init_kind) = waiting_to_init {
el(column([
el(text("Waiting to finish initializing Pijul...")),
el(text(match init_kind {
model::ProjectInitKind::New => "",
model::ProjectInitKind::ImportFromGit => {
"Importing from Git is still experimental and it might take a while"
}
})),
])
.spacing(SPACING)
.width(Length::Fill)
.height(Length::Fill))
} else {
el(column([
el(text("Select project directory:")),
dir_picker::view(
picker,
theme::Container::FadedBorder,
theme::Container::NavSelectedSection,
theme::Button::Normal,
theme::Button::Selected,
theme::Scrollable::Normal,
theme::Scrollable::Selected,
theme::Text::SlightlyFaded,
)
.map(Msg::PickingRepoDir),
])
.spacing(SPACING)
.width(Length::Fill)
.height(Length::Fill))
};
add_actions_and_report(
None,
main,
window_size,
allowed_actions,
report,
sub_menu,
false,
)
}
fn managing_repo<'a, F>(
state: &'a model::ManagingRepo,
get_diff: F,
window_size: iced::Size,
allowed_actions: &'a [action::Binding],
report: &'a report::Container,
sub_menu: &'a Option<model::SubMenu>,
) -> Element<'a, Msg, Theme>
where
F: Fn(file::IdHash) -> Option<&'a diff::File>,
{
let model::ManagingRepo { repo_path, sub } = state;
let inner = match sub {
ManagingRepoSubState::Loading { .. } => {
let main = el(container(text("Loading..."))
.width(Length::Fill)
.height(Length::Fill));
add_actions_and_report(
None,
main,
window_size,
allowed_actions,
report,
sub_menu,
false,
)
}
ManagingRepoSubState::SelectingIdentity {
ids,
selection_ix,
selection_nav,
confirmed_selection_ix,
repo: _,
} => {
let main = el(container(view_selecting_identity(
ids,
confirmed_selection_ix.unwrap_or_else(|| *selection_ix),
selection_nav,
))
.width(Length::Fill)
.height(Length::Fill));
add_actions_and_report(
None,
main,
window_size,
allowed_actions,
report,
sub_menu,
false,
)
}
ManagingRepoSubState::Ready(state) => view_ready(
window_size,
repo_path,
state,
get_diff,
allowed_actions,
report,
sub_menu,
),
ManagingRepoSubState::NoIdFound { repo: _ } => {
let main = el(container(text("No Pijul identity found. Create a new one in with Pijul CLI and then reload it here."))
.width(Length::Fill)
.height(Length::Fill));
add_actions_and_report(
None,
main,
window_size,
allowed_actions,
report,
sub_menu,
false,
)
}
};
el(container(inner)
.class(theme::Container::AppBg)
.width(Length::Fill)
.height(Length::Fill)
.padding(Padding::from([2, 5])))
}
fn push_selecting_remote<'a>(
state: &'a model::State,
window_size: iced::Size,
allowed_actions: &'a [action::Binding],
report: &'a report::Container,
remote: &'a Option<String>,
sub_menu: &'a Option<model::SubMenu>,
) -> Element<'a, Msg, Theme> {
view_selecting_remote(
state,
window_size,
allowed_actions,
report,
remote,
sub_menu,
Msg::SubMenuPushSelectRemote,
)
}
fn pull_selecting_remote<'a>(
state: &'a model::State,
window_size: iced::Size,
allowed_actions: &'a [action::Binding],
report: &'a report::Container,
remote: &'a Option<String>,
sub_menu: &'a Option<model::SubMenu>,
) -> Element<'a, Msg, Theme> {
view_selecting_remote(
state,
window_size,
allowed_actions,
report,
remote,
sub_menu,
Msg::SubMenuPullSelectRemote,
)
}
fn compare_remote_selecting_remote<'a>(
state: &'a model::State,
window_size: iced::Size,
allowed_actions: &'a [action::Binding],
report: &'a report::Container,
remote: &'a Option<String>,
sub_menu: &'a Option<model::SubMenu>,
) -> Element<'a, Msg, Theme> {
view_selecting_remote(
state,
window_size,
allowed_actions,
report,
remote,
sub_menu,
Msg::SubMenuCompareRemoteSelectRemote,
)
}
fn compare_remote_input_remote_channel<'a>(
window_size: iced::Size,
allowed_actions: &'a [action::Binding],
report: &'a report::Container,
remote: &'a Option<String>,
sub_menu: &'a Option<model::SubMenu>,
) -> Element<'a, Msg, Theme> {
view_input_remote_channel(
window_size,
allowed_actions,
report,
remote,
sub_menu,
Msg::SubMenuCompareRemoteInputRemoteChannel,
)
}
fn view_selecting_remote<'a>(
state: &'a model::State,
window_size: iced::Size,
allowed_actions: &'a [action::Binding],
report: &'a report::Container,
remote: &'a Option<String>,
sub_menu: &'a Option<model::SubMenu>,
on_select: impl FnOnce(String) -> Msg + Copy + 'static,
) -> Element<'a, Msg, Theme> {
let main = if let model::SubState::ManagingRepo(state) = &state.sub
&& let Some(model::ReadyState { repo, .. }) = model::is_ready(state)
{
let repo::State {
remotes: repo::Remotes { default, other },
..
} = repo;
let mut cols =
Vec::with_capacity(1 + default.iter().len() + other.len());
cols.push(view_header("Select remote:"));
for name in default.iter().chain(other) {
cols.push(el(button(text(name))
.on_press_with(move || on_select(name.clone()))
.class(if Some(name) == remote.as_ref() {
theme::Button::Selected
} else {
theme::Button::Normal
})));
}
el(column(cols))
} else {
el(row([]))
};
let main = el(container(main).width(Length::Fill).height(Length::Fill));
add_actions_and_report(
None,
main,
window_size,
allowed_actions,
report,
sub_menu,
false,
)
}
fn view_input_remote_channel<'a>(
window_size: iced::Size,
allowed_actions: &'a [action::Binding],
report: &'a report::Container,
channel: &'a Option<String>,
sub_menu: &'a Option<model::SubMenu>,
on_input: impl Fn(String) -> Msg + Copy + 'static,
) -> Element<'a, Msg, Theme> {
let main = el(column([
view_header("Name of a remote channel:"),
el(text("Hint: to view a changes from e.g. a discussion number 42 enter \"name:42\".")),
el(text_input(
"",
channel
.as_ref()
.map(Cow::Borrowed)
.unwrap_or_default()
.as_ref(),
)
.on_input(on_input)),
]));
let main = el(container(main).width(Length::Fill).height(Length::Fill));
add_actions_and_report(
None,
main,
window_size,
allowed_actions,
report,
sub_menu,
false,
)
}
fn view_selecting_identity<'a>(
ids: &'a [Id],
selection_ix: usize,
selection_nav: &'a nav_scrollable::State,
) -> Element<'a, Msg, Theme> {
el(column([
el(text("Select identity:")),
el(nav_scrollable(
selection_nav,
ids.iter().enumerate().map(|(ix, id)| {
el(button(text(id.name.to_string()))
.on_press(Msg::SelectIdentity(ix))
.class(if ix == selection_ix {
theme::Button::Selected
} else {
theme::Button::Normal
}))
}),
)),
]))
}
fn add_actions_and_report<'a>(
above_actions: Option<Column<'a, Msg, Theme>>,
main: Element<'a, Msg, Theme>,
window_size: iced::Size,
allowed_actions: &'a [action::Binding],
report: &'a report::Container,
sub_menu: &'a Option<model::SubMenu>,
force_highlight_actions: bool,
) -> Element<'a, Msg, Theme> {
let mut actions_inner =
above_actions.unwrap_or_else(|| column([]).spacing(4));
if let Some(sub) = sub_menu {
match sub {
model::SubMenu::Push { opt: _, remote } => {
if let Some(remote) = remote {
actions_inner = actions_inner
.push(el(text(format!("Remote: {remote}"))));
}
}
model::SubMenu::Pull { opt: _, remote } => {
if let Some(remote) = remote {
actions_inner = actions_inner
.push(el(text(format!("Remote: {remote}"))));
}
}
model::SubMenu::CompareRemote {
opt: _,
remote,
remote_channel,
} => {
if let Some(remote) = remote {
actions_inner = actions_inner
.push(el(text(format!("Remote: {remote}"))));
}
if let Some(channel) = remote_channel {
actions_inner = actions_inner
.push(el(text(format!("Remote channel: {channel}"))));
}
}
model::SubMenu::ResetChange
| model::SubMenu::InitRepo { path: _ }
| model::SubMenu::ImportFromGit { path: _ }
| model::SubMenu::Add { recursive: _ } => {}
}
}
let highlight_actions = force_highlight_actions || sub_menu.is_some();
let actions_inner = actions_inner.push(view_actions(allowed_actions));
let actions = el(container(actions_inner)
.class(if highlight_actions {
theme::Container::ActionsHighlightBg
} else {
theme::Container::ActionsBg
})
.width(Length::Fill)
.height(Length::Shrink)
.padding(Padding::from([2, 5])));
let main = overlay_report(main, report, &window_size);
el(column([main, actions]).spacing(SPACING))
}
fn view_ready<'a, F>(
window_size: iced::Size,
_repo_path: &'a Path,
state: &'a ReadyState,
get_diff: F,
allowed_actions: &'a [action::Binding],
report: &'a report::Container,
sub_menu: &'a Option<model::SubMenu>,
) -> Element<'a, Msg, Theme>
where
F: Fn(file::IdHash) -> Option<&'a diff::File>,
{
let ReadyState {
user_id: _,
repo:
repo::State {
dir_name,
channel,
other_channels,
untracked_files,
changed_files,
short_log,
remotes: _,
},
selection,
navigation,
record_changes: record_msg,
forking_channel_name,
logs,
to_record,
jobs,
record_dichotomy,
} = state;
let selection = selection::unify(selection);
let has_other_channels = !other_channels.is_empty();
let view_repo_info = || {
el(row([
el(text(dir_name)),
el(text(", channel: ")),
el(button(text(channel))
.on_press_maybe(has_other_channels.then_some(Msg::Action(
action::FilteredMsg::SelectChannel,
)))),
])
.align_y(alignment::Vertical::Center))
};
let view_untracked_files = || {
untracked_files.iter().enumerate().map(|(ix, file)| {
let selection = match selection {
selection::Unified::Status(Some(
selection::Status::UntrackedFile {
ix: selected_ix,
diff_selected,
..
},
)) if &ix == selected_ix => {
if *diff_selected {
HierarchicalSelection::ChildSelected
} else {
HierarchicalSelection::Selected
}
}
_ => HierarchicalSelection::NotSelected,
};
el(button(
text(file.to_string())
.shaping(text::Shaping::Advanced)
.wrapping(text::Wrapping::WordOrGlyph),
)
.on_press(Msg::UnfilteredSelection(
selection::UnfilteredMsg::Select(
selection::Select::UntrackedFile {
ix,
path: file.clone(),
},
),
))
.class(hierarchical_button_class(selection)))
})
};
let view_changed_files = || {
changed_files
.iter()
.enumerate()
.map(|(ix, (file, _diffs))| {
let state = to_record::determine_file(to_record, file);
let to_record_toggle = el(checkbox::three_way(state)
.on_press_with(|| {
Msg::ToRecord(to_record::Msg::ToggleFile {
path: file.clone(),
})
}));
let selection = match selection {
selection::Unified::Status(Some(
selection::Status::ChangedFile {
ix: selected_ix,
diff_selected,
..
},
)) if &ix == selected_ix => {
if *diff_selected {
HierarchicalSelection::ChildSelected
} else {
HierarchicalSelection::Selected
}
}
_ => HierarchicalSelection::NotSelected,
};
let file_name_view = el(button(
text(file.to_string())
.shaping(text::Shaping::Advanced)
.wrapping(text::Wrapping::WordOrGlyph),
)
.on_press(Msg::UnfilteredSelection(
selection::UnfilteredMsg::Select(
selection::Select::ChangedFile {
ix,
path: file.clone(),
},
),
))
.class(hierarchical_button_class(selection)));
el(row([to_record_toggle, file_name_view]))
})
};
let view_status_log = || {
short_log.iter().enumerate().map(|(ix, entry)| {
let selection = match selection {
selection::Unified::Status(Some(
selection::Status::LogChange(selection::LogChange {
ix: selected_ix,
file,
..
}),
)) if &ix == selected_ix => {
if file.is_some() {
HierarchicalSelection::ChildSelected
} else {
HierarchicalSelection::Selected
}
}
_ => HierarchicalSelection::NotSelected,
};
view_log_change_selection(ix, entry, selection)
})
};
let record_msg_editor = || {
if let Some(RecordChanges::Typing { msg, desc }) = record_msg.as_ref() {
el(column([
el(column([
view_header("Short message:"),
el(text_input("Type short message here...", msg)
.on_input(Msg::EditRecordMsg)),
])),
view_header("Longer description (optional):"),
el(column([el(text_editor(desc)
.placeholder("Type longer description here...")
.on_action(Msg::EditRecordDesc))])),
el(column([el(row([
el(button(text("Save")).on_press(Msg::Action(
action::FilteredMsg::SaveRecord,
))),
el(button(text("Postpone")).on_press(Msg::Action(
action::FilteredMsg::PostponeRecord,
))),
el(button(text("Discard")).on_press(Msg::Action(
action::FilteredMsg::DiscardRecord,
))),
]))])),
])
.spacing(SPACING))
} else {
el(row([]))
}
};
let selection_details = || {
if let selection::Unified::Status(selection) = selection {
match selection {
Some(selection::Status::UntrackedFile {
ix: _,
path,
diff_selected,
}) => {
let file_id =
file::id_parts_hash(path, file::Kind::Untracked);
let diff = get_diff(file_id);
let state = navigation.files_diffs.diffs.get(&file_id);
let nav = &navigation.files_diffs.diffs_nav;
let diffs = match diff.zip(state) {
Some((file, state)) => diff::view(
state,
nav,
file,
Some(path),
*diff_selected,
false,
to_record,
),
None => el(text("Loading diff...")),
};
el(column([
view_header(format!(
"Untracked {} contents:",
path.raw
)),
diffs,
])
.spacing(SPACING))
}
Some(selection::Status::ChangedFile {
path,
ix: _,
diff_selected,
}) => {
let file_id =
file::id_parts_hash(path, file::Kind::Changed);
let diff = get_diff(file_id);
let state = navigation.files_diffs.diffs.get(&file_id);
let nav = &navigation.files_diffs.diffs_nav;
let diffs = match diff.zip(state) {
Some((file, state)) => diff::view(
state,
nav,
file,
Some(path),
*diff_selected,
true,
to_record,
),
None => el(text("Loading diff...")),
};
el(column([
view_header(format!("{} diff:", path.raw)),
diffs,
])
.spacing(SPACING))
}
Some(selection::Status::LogChange(selection::LogChange {
ix,
hash: _,
message: _,
file,
})) => {
let entry = short_log.get(*ix).unwrap();
let change_selected = match file.as_ref() {
Some(selection::LogChangeFileSelection {
ix: _,
path: _,
diff_selected,
}) => !*diff_selected,
_ => false,
};
let files = entry.file_paths.iter().enumerate().map(
|(ix, path)| {
let selection = match file {
Some(selection::LogChangeFileSelection {
path: selected_path,
diff_selected,
..
}) if selected_path == path => {
if *diff_selected {
HierarchicalSelection::ChildSelected
} else {
HierarchicalSelection::Selected
}
}
_ => HierarchicalSelection::NotSelected,
};
el(button(text(path))
.on_press_with(move || {
Msg::UnfilteredSelection(
selection::UnfilteredMsg::Select(
selection::Select::LogChangeFile {
ix,
path: path.clone(),
},
),
)
})
.class(hierarchical_button_class(selection)))
},
);
let nav = &navigation.status_logs_navs.files_nav;
let files = el(nav_scrollable(nav, files)
.class(if change_selected {
theme::Scrollable::Selected
} else {
theme::Scrollable::Normal
})
.width(Length::Fill)
.height(Length::Fill));
el(column([view_log_change_header(entry), files])
.width(Length::Fill)
.height(Length::Fill)
.spacing(SPACING))
}
None => el(row([])),
}
} else {
el(row([]))
}
};
let max_visible_cols = cmp::max(
2,
window_size.width.floor() as usize / DEFAULT_MIN_COL_WIDTH,
);
let depth = if let Some(RecordChanges::Typing { .. }) = record_msg.as_ref()
{
1
} else {
match selection {
selection::Unified::EntireLog(Some(selection::LogChange {
file,
..
})) => {
if file.is_none() {
1
} else {
2
}
}
selection::Unified::Status(Some(selection)) => match selection {
selection::Status::UntrackedFile { .. }
| selection::Status::ChangedFile { .. }
| selection::Status::LogChange(selection::LogChange {
file: None,
..
}) => 1,
selection::Status::LogChange(selection::LogChange {
file: Some(_),
..
}) => 2,
},
selection::Unified::Channel(Some(selection)) => match selection {
selection::Channel { log: None, .. } => 1,
selection::Channel {
log: Some(selection::LogChange { file: None, .. }),
..
} => 2,
selection::Channel {
log: Some(selection::LogChange { file: Some(_), .. }),
..
} => 3,
},
selection::Unified::CompareRemote(Some(
selection::CompareRemote { file, .. },
)) => {
if file.is_none() {
1
} else {
2
}
}
selection::Unified::Status(None)
| selection::Unified::Channel(None)
| selection::Unified::EntireLog(None)
| selection::Unified::CompareRemote(None) => 0,
}
};
let hidden_cols = (depth + 1_usize).saturating_sub(max_visible_cols);
let changed_files_header = el(text(if changed_files.is_empty() {
"No changed files"
} else {
"Changed files:"
}));
let changed_files_header_with_toggle = if changed_files.len() > 1 {
let to_record_toggle = el(checkbox::three_way(to_record.overall)
.on_press_with(|| Msg::ToRecord(to_record::Msg::ToggleOverall)));
el(container(el(row([to_record_toggle, changed_files_header])
.align_y(Alignment::Center)
.spacing(SPACING)))
.padding(Padding::ZERO.top(SPACING)))
} else {
changed_files_header
};
let status_nav_children = || {
[el(container(el(text(if untracked_files.is_empty() {
"No untracked files"
} else {
"Untracked files:"
})))
.padding(Padding::ZERO.top(SPACING)))]
.into_iter()
.chain(view_untracked_files())
.chain([changed_files_header_with_toggle])
.chain(view_changed_files())
.chain([el(container(el(text("Recent records:")))
.padding(Padding::ZERO.top(SPACING)))])
.chain(view_status_log())
};
let status_selected =
|| match selection {
selection::Unified::Status(Some(
selection::Status::UntrackedFile { diff_selected, .. },
)) => !diff_selected,
selection::Unified::Status(Some(
selection::Status::ChangedFile { diff_selected, .. },
)) => !diff_selected,
selection::Unified::Status(Some(selection::Status::LogChange(
selection::LogChange { file, .. },
))) => file.is_none(),
selection::Unified::CompareRemote(Some(
selection::CompareRemote { file, .. },
)) => file.is_none(),
selection::Unified::Status(None) => true,
selection::Unified::Channel(_)
| selection::Unified::EntireLog(_)
| selection::Unified::CompareRemote(_) => false,
};
let status_col_0 = || {
el(column([
view_repo_info(),
el(
nav_scrollable(&navigation.status_nav, status_nav_children())
.class(if status_selected() {
theme::Scrollable::Selected
} else {
theme::Scrollable::Normal
})
.width(Length::Fill)
.height(Length::Fill),
),
])
.width(Length::Fill)
.height(Length::Fill))
};
let status_col_1 = || {
el(column([
el(column([record_msg_editor(), selection_details()])
.width(Length::Fill)
.height(Length::Fill)),
if hidden_cols == 1 {
el(button(row([el(
text("← Status").shaping(text::Shaping::Advanced)
)]))
.on_press(Msg::Action(
action::FilteredMsg::Selection(selection::Msg::PressDir(
selection::Dir::Left,
)),
)))
} else {
el(row([]))
},
])
.width(Length::Fill)
.height(Length::Fill)
.spacing(SPACING))
};
let status_col_2 = || match selection {
selection::Unified::Status(Some(selection::Status::LogChange(
selection::LogChange {
ix: _,
hash,
message: _,
file:
Some(selection::LogChangeFileSelection {
ix: _,
path,
diff_selected,
}),
},
))) => {
let file_id = file::log_id_parts_hash(*hash, path);
let state = navigation.log_diffs.diffs.get(&file_id);
el(column([
el(column([
view_header(format!(
"{path} changes in {}:",
display_short_hash(hash)
)),
match state {
Some(diff::FileAndState { file, state }) => {
let nav = &navigation.status_logs_navs.diffs_nav;
diff::view(
state,
nav,
file,
None,
*diff_selected,
false,
to_record,
)
}
None => el(text("Loading diff..")),
},
])
.spacing(SPACING)),
if hidden_cols == 2 {
el(button(row([el(
text("← Files").shaping(text::Shaping::Advanced)
)]))
.on_press(Msg::Action(
action::FilteredMsg::Selection(
selection::Msg::PressDir(selection::Dir::Left),
),
)))
} else {
el(row([]))
},
])
.width(Length::Fill)
.height(Length::Fill)
.spacing(SPACING))
}
_ => el(column([])),
};
let other_channels_selected = || match selection {
selection::Unified::Channel(channel) => match channel {
Some(selection::Channel { log, .. }) => log.is_none(),
None => true,
},
_ => false,
};
let other_channel_log_selected = || match selection {
selection::Unified::Channel(Some(selection::Channel {
ix: _,
name: _,
log: Some(selection::LogChange { file, .. }),
})) => file.is_none(),
_ => false,
};
let other_channel_log_change_selected = || match selection {
selection::Unified::Channel(Some(selection::Channel {
ix: _,
name: _,
log:
Some(selection::LogChange {
file:
Some(selection::LogChangeFileSelection {
diff_selected,
..
}),
..
}),
})) => !*diff_selected,
_ => false,
};
let view_channels = || {
other_channels.iter().enumerate().map(|(ix, channel)| {
let selection = match selection {
selection::Unified::Channel(Some(selection::Channel {
ix: selected_ix,
log,
..
})) if &ix == selected_ix => {
if log.is_some() {
HierarchicalSelection::ChildSelected
} else {
HierarchicalSelection::Selected
}
}
_ => HierarchicalSelection::NotSelected,
};
el(button(text(channel))
.on_press(Msg::UnfilteredSelection(
selection::UnfilteredMsg::Select(
selection::Select::Channel {
ix,
name: channel.clone(),
},
),
))
.class(hierarchical_button_class(selection)))
})
};
let channel_col_0 = || {
el(column([
el(text(format!("Current channel: {channel}"))),
el(
nav_scrollable(&navigation.other_channels_nav, view_channels())
.class(if other_channels_selected() {
theme::Scrollable::Selected
} else {
theme::Scrollable::Normal
})
.width(Length::Fill)
.height(Length::Fill),
),
]))
};
let channel_col_1 = || match selection {
selection::Unified::Channel(Some(selection::Channel {
ix: _channel_ix,
name,
log: _,
})) => {
let Some(Log::Loaded { log }) = logs.other_channels_logs.get(name)
else {
return el(column([]));
};
let entries = log.iter().enumerate().map(|(ix, entry)| {
let selection = match selection {
selection::Unified::Channel(Some(selection::Channel {
log:
Some(selection::LogChange {
ix: selected_ix,
file,
..
}),
..
})) if &ix == selected_ix => {
if file.is_some() {
HierarchicalSelection::ChildSelected
} else {
HierarchicalSelection::Selected
}
}
_ => HierarchicalSelection::NotSelected,
};
view_log_change_selection(ix, entry, selection)
});
let len = log.len();
let selected_ix = match selection {
selection::Unified::EntireLog(Some(selection::LogChange {
ix,
..
})) => Some(len - *ix),
_ => None,
};
el(column([
if let Some(selected_ix) = selected_ix {
el(text(format!(
"Channel {name} log ({selected_ix}/{len})"
)))
} else {
el(text(format!("Channel {name} log ({len})")))
},
el(nav_scrollable(&navigation.other_channel_log_nav, entries)
.class(if other_channel_log_selected() {
theme::Scrollable::Selected
} else {
theme::Scrollable::Normal
})
.width(Length::Fill)
.height(Length::Fill)),
if hidden_cols == 1 {
el(button(row([el(text("← Other channels")
.shaping(text::Shaping::Advanced))]))
.on_press(Msg::Action(action::FilteredMsg::Selection(
selection::Msg::PressDir(selection::Dir::Left),
))))
} else {
el(row([]))
},
])
.width(Length::Fill)
.height(Length::Fill))
}
_ => el(column([])),
};
let channel_col_2 = || match selection {
selection::Unified::Channel(Some(selection::Channel {
ix: _channel_ix,
name,
log:
Some(selection::LogChange {
ix,
hash: _,
message: _,
file,
}),
})) => {
let nav = &navigation.other_channel_logs_navs.files_nav;
let files_view = match logs.other_channels_logs.get(name) {
Some(Log::Loaded { log }) => {
let entry = log.get(*ix).unwrap();
let files = entry.file_paths.iter().enumerate().map(
|(ix, path)| {
let selection = match file {
Some(selection::LogChangeFileSelection {
path: selected_path,
diff_selected,
..
}) if selected_path == path => {
if *diff_selected {
HierarchicalSelection::ChildSelected
} else {
HierarchicalSelection::Selected
}
}
_ => HierarchicalSelection::NotSelected,
};
el(button(text(path))
.on_press_with(move || {
Msg::UnfilteredSelection(
selection::UnfilteredMsg::Select(
selection::Select::LogChangeFile {
ix,
path: path.clone(),
},
),
)
})
.class(hierarchical_button_class(selection)))
},
);
el(column([
view_log_change_header(entry),
el(nav_scrollable(nav, files)
.class(if other_channel_log_change_selected() {
theme::Scrollable::Selected
} else {
theme::Scrollable::Normal
})
.width(Length::Fill)
.height(Length::Fill)),
]))
}
_ => el(text("Loading...")),
};
el(column([
files_view,
if hidden_cols == 2 {
el(button(row([el(
text("← Log").shaping(text::Shaping::Advanced)
)]))
.on_press(Msg::Action(
action::FilteredMsg::Selection(
selection::Msg::PressDir(selection::Dir::Left),
),
)))
} else {
el(row([]))
},
])
.width(Length::Fill)
.height(Length::Fill)
.spacing(SPACING))
}
_ => el(row([])),
};
let channel_col_3 = || match selection {
selection::Unified::Channel(Some(selection::Channel {
name: _,
log:
Some(selection::LogChange {
ix: _,
hash,
message: _,
file:
Some(selection::LogChangeFileSelection {
ix: _,
path,
diff_selected,
}),
}),
..
})) => {
let file_id = file::log_id_parts_hash(*hash, path);
let state = navigation.log_diffs.diffs.get(&file_id);
el(column([
el(column([
view_header(format!(
"{path} changes in {}:",
display_short_hash(hash)
)),
match state {
Some(diff::FileAndState { file, state }) => {
let nav =
&navigation.other_channel_logs_navs.diffs_nav;
diff::view(
state,
nav,
file,
None,
*diff_selected,
false,
to_record,
)
}
None => el(text("Loading diff..")),
},
])
.spacing(SPACING)),
if hidden_cols == 3 {
el(button(row([el(
text("← Files").shaping(text::Shaping::Advanced)
)]))
.on_press(Msg::Action(
action::FilteredMsg::Selection(
selection::Msg::PressDir(selection::Dir::Left),
),
)))
} else {
el(row([]))
},
])
.width(Length::Fill)
.height(Length::Fill)
.spacing(SPACING))
}
_ => el(column([])),
};
let entire_log_selected = || match selection {
selection::Unified::EntireLog(log_change) => match log_change {
Some(selection::LogChange { file, .. }) => file.is_none(),
None => true,
},
_ => false,
};
let entire_log_change_selected = || match selection {
selection::Unified::EntireLog(Some(selection::LogChange {
file: Some(selection::LogChangeFileSelection { diff_selected, .. }),
..
})) => !*diff_selected,
_ => false,
};
let entire_log_col_0 = || {
let Some(Log::Loaded { log }) = logs.entire_log.as_ref() else {
unreachable!()
};
let entries = log.iter().enumerate().map(|(ix, entry)| {
let selection = match selection {
selection::Unified::EntireLog(Some(selection::LogChange {
ix: selected_ix,
file,
..
})) if &ix == selected_ix => {
if file.is_some() {
HierarchicalSelection::ChildSelected
} else {
HierarchicalSelection::Selected
}
}
_ => HierarchicalSelection::NotSelected,
};
view_log_change_selection(ix, entry, selection)
});
let len = log.len();
let selected_ix = match selection {
selection::Unified::EntireLog(Some(selection::LogChange {
ix,
..
})) => Some(len - *ix),
_ => None,
};
el(column([
view_repo_info(),
if let Some(selected_ix) = selected_ix {
el(text(format!("Entire log ({selected_ix}/{len})")))
} else {
el(text(format!("Entire log ({len})")))
},
el(nav_scrollable(&navigation.entire_log_nav, entries)
.class(if entire_log_selected() {
theme::Scrollable::Selected
} else {
theme::Scrollable::Normal
})
.width(Length::Fill)
.height(Length::Fill)),
])
.width(Length::Fill)
.height(Length::Fill))
};
let entire_log_col_1 = || match selection {
selection::Unified::EntireLog(Some(selection::LogChange {
ix,
hash: _,
message: _,
file,
})) => {
let files_view = match logs.entire_log.as_ref() {
Some(Log::Loaded { log }) => {
let nav = &navigation.entire_logs_navs.files_nav;
let entry = log.get(*ix).unwrap();
let files = entry.file_paths.iter().enumerate().map(
|(ix, path)| {
let selection = match file {
Some(selection::LogChangeFileSelection {
path: selected_path,
diff_selected,
..
}) if selected_path == path => {
if *diff_selected {
HierarchicalSelection::ChildSelected
} else {
HierarchicalSelection::Selected
}
}
_ => HierarchicalSelection::NotSelected,
};
el(button(text(path))
.on_press_with(move || {
Msg::UnfilteredSelection(
selection::UnfilteredMsg::Select(
selection::Select::LogChangeFile {
ix,
path: path.clone(),
},
),
)
})
.class(hierarchical_button_class(selection)))
},
);
el(column([
view_log_change_header(entry),
el(nav_scrollable(nav, files)
.class(if entire_log_change_selected() {
theme::Scrollable::Selected
} else {
theme::Scrollable::Normal
})
.width(Length::Fill)
.height(Length::Fill)),
]))
}
_ => el(text("Loading...")),
};
el(column([
files_view,
if hidden_cols == 1 {
el(button(row([el(
text("← Log").shaping(text::Shaping::Advanced)
)]))
.on_press(Msg::Action(
action::FilteredMsg::Selection(
selection::Msg::PressDir(selection::Dir::Left),
),
)))
} else {
el(row([]))
},
])
.width(Length::Fill)
.height(Length::Fill)
.spacing(SPACING))
}
_ => el(row([])),
};
let entire_log_col_2 = || match selection {
selection::Unified::EntireLog(Some(selection::LogChange {
ix: _,
hash,
message: _,
file:
Some(selection::LogChangeFileSelection {
ix: _,
path,
diff_selected,
}),
})) => {
let file_id = file::log_id_parts_hash(*hash, path);
let state = navigation.log_diffs.diffs.get(&file_id);
el(column([
el(column([
view_header(format!(
"{path} changes in {}:",
display_short_hash(hash)
)),
match state {
Some(diff::FileAndState { file, state }) => {
let nav = &navigation.entire_logs_navs.diffs_nav;
diff::view(
state,
nav,
file,
None,
*diff_selected,
false,
to_record,
)
}
None => el(text("Loading diff..")),
},
])
.spacing(SPACING)),
if hidden_cols == 2 {
el(button(row([el(
text("← Files").shaping(text::Shaping::Advanced)
)]))
.on_press(Msg::Action(
action::FilteredMsg::Selection(
selection::Msg::PressDir(selection::Dir::Left),
),
)))
} else {
el(row([]))
},
])
.width(Length::Fill)
.height(Length::Fill)
.spacing(SPACING))
}
_ => el(column([])),
};
let compare_remote_nav_children = || {
if let selection::Unified::CompareRemote(Some(
selection::CompareRemote {
ix: _,
hash: _,
file: _,
remote,
remote_channel,
},
)) = selection
&& let Some(record_dichotomy) = model::get_record_dichotomy(
record_dichotomy,
remote,
remote_channel,
)
{
let repo::RecordDichotomy {
local_records,
remote_records,
remote_unrecords,
} = record_dichotomy;
let count = |records: &[_]| {
records.len() + if records.is_empty() { 0 } else { 1 } };
let row_count = count(local_records)
+ count(remote_records)
+ count(remote_unrecords);
if row_count == 0 {
vec![el(text("Local and remote are the same!"))]
} else {
let mut rows = Vec::with_capacity(row_count);
let view = |rows: &mut Vec<_>, ix, entry: &repo::LogEntry| {
let selection = match selection {
selection::Unified::CompareRemote(Some(
selection::CompareRemote {
ix: Some(selected_ix),
hash: _,
file,
remote: _,
remote_channel: _,
},
)) if &ix == selected_ix => {
if file.is_some() {
HierarchicalSelection::ChildSelected
} else {
HierarchicalSelection::Selected
}
}
_ => HierarchicalSelection::NotSelected,
};
rows.push(el(button(el(text(display_short_hash(
&entry.hash,
))
.font(Font::MONOSPACE)))
.on_press(Msg::UnfilteredSelection(
selection::UnfilteredMsg::Select(
selection::Select::CompareRemote { ix },
),
))
.class(hierarchical_button_class(selection))));
};
if !local_records.is_empty() {
rows.push(el(container(el(text("Local records:")))
.padding(Padding::ZERO.top(SPACING))));
}
let ix_offset = 0;
record_dichotomy.local_records.iter().enumerate().for_each(
|(ix, entry)| view(&mut rows, ix + ix_offset, entry),
);
if !remote_records.is_empty() {
rows.push(el(container(el(text("Remote records:")))
.padding(Padding::ZERO.top(SPACING))));
}
let ix_offset = local_records.len();
record_dichotomy.remote_records.iter().enumerate().for_each(
|(ix, entry)| view(&mut rows, ix + ix_offset, entry),
);
if !remote_unrecords.is_empty() {
rows.push(el(container(el(text("Remote unrecords:")))
.padding(Padding::ZERO.top(SPACING))));
}
let ix_offset = ix_offset + remote_records.len();
record_dichotomy
.remote_unrecords
.iter()
.enumerate()
.for_each(|(ix, entry)| {
view(&mut rows, ix + ix_offset, entry)
});
rows
}
} else {
vec![el(text("Loading..."))]
}
};
let compare_remote_col_0 = || {
el(column([el(nav_scrollable(
&navigation.compare_remote_nav,
compare_remote_nav_children(),
)
.class(if status_selected() {
theme::Scrollable::Selected
} else {
theme::Scrollable::Normal
})
.width(Length::Fill)
.height(Length::Fill))])
.width(Length::Fill)
.height(Length::Fill))
};
let compare_remote_col_1 = || match selection {
selection::Unified::CompareRemote(Some(selection::CompareRemote {
ix: Some(ix),
hash: _,
file,
remote,
remote_channel,
})) => {
let files_view = if let Some(record_dichotomy) =
model::get_record_dichotomy(
record_dichotomy,
remote,
remote_channel,
) {
let nav = &navigation.compare_remote_navs.files_nav;
let entry = record_dichotomy.get(*ix).unwrap();
let files =
entry.file_paths.iter().enumerate().map(|(ix, path)| {
let selection = match file {
Some(selection::LogChangeFileSelection {
path: selected_path,
diff_selected,
..
}) if selected_path == path => {
if *diff_selected {
HierarchicalSelection::ChildSelected
} else {
HierarchicalSelection::Selected
}
}
_ => HierarchicalSelection::NotSelected,
};
el(button(text(path))
.on_press_with(move || {
Msg::UnfilteredSelection(
selection::UnfilteredMsg::Select(
selection::Select::CompareRemoteFile {
ix,
path: path.clone(),
},
),
)
})
.class(hierarchical_button_class(selection)))
});
el(column([
view_log_change_header(entry),
el(nav_scrollable(nav, files)
.class(if entire_log_change_selected() {
theme::Scrollable::Selected
} else {
theme::Scrollable::Normal
})
.width(Length::Fill)
.height(Length::Fill)),
]))
} else {
el(text("Loading..."))
};
el(column([
files_view,
if hidden_cols == 1 {
el(button(row([el(
text("← Records").shaping(text::Shaping::Advanced)
)]))
.on_press(Msg::Action(
action::FilteredMsg::Selection(
selection::Msg::PressDir(selection::Dir::Left),
),
)))
} else {
el(row([]))
},
])
.width(Length::Fill)
.height(Length::Fill)
.spacing(SPACING))
}
_ => el(row([])),
};
let compare_remote_col_2 = || match selection {
selection::Unified::CompareRemote(Some(selection::CompareRemote {
ix: _,
hash: Some(hash),
file:
Some(selection::LogChangeFileSelection {
ix: _,
path,
diff_selected,
}),
remote: _,
remote_channel: _,
})) => {
let file_id = file::log_id_parts_hash(*hash, path);
let state = navigation.log_diffs.diffs.get(&file_id);
el(column([
el(column([
view_header(format!(
"{path} changes in {}:",
display_short_hash(hash)
)),
match state {
Some(diff::FileAndState { file, state }) => {
let nav = &navigation.compare_remote_navs.diffs_nav;
diff::view(
state,
nav,
file,
None,
*diff_selected,
false,
to_record,
)
}
None => el(text("Loading diff..")),
},
])
.spacing(SPACING)),
if hidden_cols == 2 {
el(button(row([el(
text("← Files").shaping(text::Shaping::Advanced)
)]))
.on_press(Msg::Action(
action::FilteredMsg::Selection(
selection::Msg::PressDir(selection::Dir::Left),
),
)))
} else {
el(row([]))
},
])
.width(Length::Fill)
.height(Length::Fill)
.spacing(SPACING))
}
_ => el(column([])),
};
let main = match selection {
selection::Unified::Status(_) => {
let cols = [status_col_0(), status_col_1(), status_col_2()]
.into_iter()
.skip(hidden_cols);
el(row(cols)
.spacing(SPACING)
.width(Length::Fill)
.height(Length::Fill))
}
selection::Unified::Channel(_) => {
if other_channels.is_empty() {
el(column([el(text(format!("Current channel: {channel}")))])
.width(Length::Fill)
.height(Length::Fill))
} else {
let cols = [
channel_col_0(),
channel_col_1(),
channel_col_2(),
channel_col_3(),
]
.into_iter()
.skip(hidden_cols);
el(row(cols).width(Length::Fill).height(Length::Fill))
}
}
selection::Unified::EntireLog(_) => match logs.entire_log.as_ref() {
None | Some(Log::Loading) => el(column([
view_repo_info(),
el(text("Entire log")),
el(text("Loading...")),
])
.width(Length::Fill)
.height(Length::Fill)),
Some(Log::Loaded { .. }) => {
let cols = [
entire_log_col_0(),
entire_log_col_1(),
entire_log_col_2(),
]
.into_iter()
.skip(hidden_cols);
el(row(cols)
.spacing(SPACING)
.width(Length::Fill)
.height(Length::Fill))
}
},
selection::Unified::CompareRemote(_) => {
let cols = [
compare_remote_col_0(),
compare_remote_col_1(),
compare_remote_col_2(),
]
.into_iter()
.skip(hidden_cols);
el(row(cols)
.spacing(SPACING)
.width(Length::Fill)
.height(Length::Fill))
}
};
let actions_inner = column([]).spacing(4);
let mut force_highlight_actions = false;
let actions_inner = if let Some(forking_channel) = forking_channel_name {
let channel_name_input = text_input("channel name...", forking_channel)
.on_input(Msg::EditForkChannelName);
force_highlight_actions = true;
actions_inner.push(channel_name_input)
} else {
actions_inner
};
let actions_inner = if !jobs.is_empty() {
let jobs = view_jobs(jobs, channel);
actions_inner.push(jobs)
} else {
actions_inner
};
add_actions_and_report(
Some(actions_inner),
main,
window_size,
allowed_actions,
report,
sub_menu,
force_highlight_actions,
)
}
fn overlay_report<'a>(
overlaid: Element<'a, Msg, Theme>,
report: &'a report::Container,
window_size: &iced::Size,
) -> Element<'a, Msg, Theme> {
report::view(report, overlaid, |report| {
let report_width =
if window_size.width > DEFAULT_MIN_COL_WIDTH as f32 * 1.3 {
window_size.width * 0.45
} else {
window_size.width * 0.95
};
let max_height = window_size.height * 0.85;
el(container(column([
el(row([el(container(
button(text("↧").shaping(text::Shaping::Advanced))
.on_press(Msg::Action(action::FilteredMsg::ToggleReports)),
)
.width(Length::Fill)
.align_x(Alignment::End))])
.width(Length::Fill)),
el(container(column(
report.entries.iter().rev().map(view_report_entry),
))
.padding([4, 6])
.class(theme::Container::Report)),
]))
.width(Length::Fixed(report_width))
.height(Length::Shrink)
.max_height(max_height))
})
}
fn view_report_entry<'a>(entry: &'a report::Entry) -> Element<'a, Msg, Theme>
where
Msg: 'a,
{
let report::Entry {
level,
msg,
time,
is_read,
} = entry;
let time = time.strftime("%I:%M:%S.%3f").to_string();
let text_class = if *is_read {
theme::Text::SlightlyFaded
} else {
theme::Text::Normal
};
el(row([
el(text(msg)
.width(Length::Fill)
.wrapping(text::Wrapping::WordOrGlyph)
.class(text_class)),
el(container(row([]))
.width(Length::Fixed(8.0))
.height(Length::Fill)
.class(theme::Container::ReportLevel {
level: *level,
is_read: *is_read,
})),
el(text(time)
.align_x(Alignment::End)
.size(11.0)
.class(text_class)),
])
.spacing(6)
.padding(4)
.width(Length::Fill)
.height(Length::Shrink))
}
fn view_header<'a>(
header: impl text::IntoFragment<'a>,
) -> Element<'a, Msg, Theme> {
el(text(header).font(Font {
weight: font::Weight::Bold,
..default()
}))
}
fn view_actions<'a>(
allowed_actions: &[action::Binding],
) -> Element<'a, Msg, Theme> {
let buttons: Vec<_> = allowed_actions
.iter()
.map(
|action::Binding {
keys_str,
keys: _,
label,
msg,
}| {
if let Some(msg) = msg {
action_button(keys_str, label, Msg::Action(msg.clone()))
} else {
el(action_button_inner(keys_str, label))
}
},
)
.collect();
el(row(buttons).spacing(2).wrap())
}
fn view_jobs<'a>(
jobs: &IndexSet<Job>,
current_channel: &str,
) -> Element<'a, Msg, Theme> {
let jobs: Vec<_> = itertools::intersperse_with(
jobs.iter().map(|job| view_job(job, current_channel)),
|| el(text(", ")),
)
.collect();
el(row(jobs).spacing(2).wrap())
}
fn view_job<'a>(job: &Job, current_channel: &str) -> Element<'a, Msg, Theme> {
match job {
Job::Pull { remote, channel } => {
if channel == current_channel {
el(text(format!("Pulling from remote {remote}")))
} else {
el(text(format!(
"Pulling from remote {remote}, channel {channel}"
)))
}
}
Job::Push { remote, channel } => {
if channel == current_channel {
el(text(format!("Pushing to remote {remote}")))
} else {
el(text(format!(
"Pushing to remote {remote}, channel {channel}"
)))
}
}
Job::CompareRemote {
remote,
remote_channel: channel,
} => el(text(format!(
"Fetching from remote {remote}, channel {channel}",
))),
}
}
fn action_button<'a>(
key: &'a str,
label: &'a str,
on_press: Msg,
) -> Element<'a, Msg, Theme> {
el(action_button_inner(key, label).on_press(on_press))
}
fn action_button_inner<'a>(
key: &'a str,
label: &'a str,
) -> Button<'a, Msg, Theme> {
let row = row([el(text(key)
.shaping(text::Shaping::Advanced)
.font(Font {
weight: font::Weight::Bold,
..default()
})
.class(theme::Text::HighlightOnLightBg))])
.spacing(6);
let row = if label.is_empty() {
row
} else {
row.push(el(text(label)))
};
button(row)
}
#[derive(Debug)]
enum HierarchicalSelection {
NotSelected,
Selected,
ChildSelected,
}
fn hierarchical_button_class(
selection: HierarchicalSelection,
) -> theme::Button {
match selection {
HierarchicalSelection::NotSelected => theme::Button::Normal,
HierarchicalSelection::Selected => theme::Button::Selected,
HierarchicalSelection::ChildSelected => theme::Button::ChildSelected,
}
}
fn display_short_hash(hash: &repo::ChangeHash) -> String {
let mut short_hash = repo::hash_to_string(hash);
short_hash.truncate(8);
short_hash
}
fn view_log_change_selection(
ix: usize,
repo::LogEntry {
hash,
message,
description: _,
timestamp: _,
file_paths: _,
}: &repo::LogEntry,
selection: HierarchicalSelection,
) -> Element<'_, Msg, Theme> {
let short_hash = display_short_hash(hash);
el(row([
el(button(text(short_hash).font(Font::MONOSPACE))
.on_press(Msg::UnfilteredSelection(
selection::UnfilteredMsg::Select(
selection::Select::LogChange {
ix,
hash: *hash,
message: message.clone(),
},
),
))
.class(hierarchical_button_class(selection))),
el(text(message.split("\n").next().unwrap())
.shaping(text::Shaping::Advanced)),
])
.spacing(SPACING))
}
fn view_log_change_header(
repo::LogEntry {
hash,
message,
description,
timestamp,
file_paths: _,
}: &repo::LogEntry,
) -> Element<'_, Msg, Theme> {
let short_hash = display_short_hash(hash);
let mut cols = Vec::with_capacity(3 + description.iter().len());
cols.push(el(row([
el(text(short_hash.to_string()).font(Font {
weight: font::Weight::Bold,
..Font::MONOSPACE
})),
view_timestamp(timestamp),
])
.spacing(SPACING)));
cols.push(el(row([
view_header("Message:"),
el(text(message).shaping(text::Shaping::Advanced)),
])
.spacing(SPACING)));
if let Some(description) = description {
cols.push(el(row([
view_header("Description:"),
el(text(description).shaping(text::Shaping::Advanced)),
])
.spacing(SPACING)));
}
cols.push(view_header("Files:".to_string()));
el(column(cols).spacing(SPACING))
}
fn view_timestamp(timestamp: &Timestamp) -> Element<'_, Msg, Theme> {
el(text(timestamp.strftime("%H:%M:%S, %b %d, %Y").to_string()))
}