Gallery view (#488)

* WIP: gallery view

* Adjust gallery view to design

* Update dialog to better match gallery design
This commit is contained in:
Jeremy Soller 2024-09-23 15:56:32 -06:00 committed by GitHub
parent eda1189f08
commit 7b2e448947
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 216 additions and 61 deletions

View file

@ -1360,6 +1360,14 @@ impl Application for App {
return Command::none(); return Command::none();
} }
// Close gallery mode if open
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
if tab.gallery {
tab.gallery = false;
return Command::none();
}
}
// Close menus and context panes in order per message // Close menus and context panes in order per message
// Why: It'd be weird to close everything all at once // Why: It'd be weird to close everything all at once
// Usually, the Escape key (for example) closes menus and panes one by one instead // Usually, the Escape key (for example) closes menus and panes one by one instead
@ -2392,6 +2400,9 @@ impl Application for App {
tab::Command::PreviewCancel => { tab::Command::PreviewCancel => {
self.preview_opt = None; self.preview_opt = None;
} }
tab::Command::WindowDrag => {
commands.push(window::drag(self.main_window_id()));
}
} }
} }
return Command::batch(commands); return Command::batch(commands);
@ -2772,6 +2783,17 @@ impl Application for App {
} }
fn dialog(&self) -> Option<Element<Message>> { fn dialog(&self) -> Option<Element<Message>> {
//TODO: should gallery view just be a dialog?
let entity = self.tab_model.active();
if let Some(tab) = self.tab_model.data::<Tab>(entity) {
if tab.gallery {
return Some(
tab.gallery_view()
.map(move |tab_message| Message::TabMessage(Some(entity), tab_message)),
);
}
}
let dialog_page = match self.dialog_pages.front() { let dialog_page = match self.dialog_pages.front() {
Some(some) => some, Some(some) => some,
None => return None, None => return None,

View file

@ -389,6 +389,62 @@ struct App {
} }
impl App { impl App {
fn button_row(&self) -> Element<Message> {
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
let mut row = widget::row::with_capacity(
if !self.filters.is_empty() { 1 } else { 0 } + self.choices.len() * 2 + 3,
)
.align_items(Alignment::Center)
.padding(space_xxs)
.spacing(space_xxs);
if !self.filters.is_empty() {
row = row.push(widget::dropdown(
&self.filters,
self.filter_selected,
Message::Filter,
));
}
for (choice_i, choice) in self.choices.iter().enumerate() {
match choice {
DialogChoice::CheckBox { label, value, .. } => {
row = row.push(widget::checkbox(label, *value, move |checked| {
Message::Choice(choice_i, if checked { 1 } else { 0 })
}));
}
DialogChoice::ComboBox {
label,
options,
selected,
..
} => {
row = row.push(widget::text::heading(label));
row = row.push(widget::dropdown(options, *selected, move |option_i| {
Message::Choice(choice_i, option_i)
}));
}
}
}
if let DialogKind::SaveFile { filename } = &self.flags.kind {
row = row.push(
widget::text_input("", filename)
.id(self.filename_id.clone())
.on_input(Message::Filename)
.on_submit(Message::Save(false)),
);
} else {
row = row.push(widget::horizontal_space(Length::Fill));
}
row = row.push(widget::button::standard(fl!("cancel")).on_press(Message::Cancel));
row = row.push(if self.flags.kind.save() {
widget::button::suggested(&self.accept_label).on_press(Message::Save(false))
} else {
widget::button::suggested(&self.accept_label).on_press(Message::Open)
});
row.into()
}
fn preview(&self, kind: &PreviewKind) -> Element<AppMessage> { fn preview(&self, kind: &PreviewKind) -> Element<AppMessage> {
let mut children = Vec::with_capacity(1); let mut children = Vec::with_capacity(1);
match kind { match kind {
@ -701,6 +757,21 @@ impl Application for App {
} }
fn dialog(&self) -> Option<Element<Message>> { fn dialog(&self) -> Option<Element<Message>> {
//TODO: should gallery view just be a dialog?
if self.tab.gallery {
return Some(
widget::column::with_children(vec![
self.tab.gallery_view().map(Message::TabMessage),
// Draw button row as part of the overlay
widget::container(self.button_row())
.width(Length::Fill)
.style(theme::Container::WindowBackground)
.into(),
])
.into(),
);
}
let dialog_page = match self.dialog_pages.front() { let dialog_page = match self.dialog_pages.front() {
Some(some) => some, Some(some) => some,
None => return None, None => return None,
@ -877,6 +948,12 @@ impl Application for App {
} }
fn on_escape(&mut self) -> Command<Message> { fn on_escape(&mut self) -> Command<Message> {
if self.tab.gallery {
// Close gallery if open
self.tab.gallery = false;
return Command::none();
}
if self.search_active { if self.search_active {
// Close search if open // Close search if open
self.search_active = false; self.search_active = false;
@ -1279,6 +1356,9 @@ impl Application for App {
tab::Command::PreviewCancel => { tab::Command::PreviewCancel => {
self.preview_opt = None; self.preview_opt = None;
} }
tab::Command::WindowDrag => {
commands.push(window::drag(self.main_window_id()));
}
unsupported => { unsupported => {
log::warn!("{unsupported:?} not supported in dialog mode"); log::warn!("{unsupported:?} not supported in dialog mode");
} }
@ -1359,9 +1439,8 @@ impl Application for App {
/// Creates a view after each update. /// Creates a view after each update.
fn view(&self) -> Element<Message> { fn view(&self) -> Element<Message> {
let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
let mut tab_column = widget::column::with_capacity(2); let mut tab_column = widget::column::with_capacity(2);
tab_column = tab_column.push( tab_column = tab_column.push(
//TODO: key binds for dialog //TODO: key binds for dialog
self.tab self.tab
@ -1369,57 +1448,7 @@ impl Application for App {
.map(move |message| Message::TabMessage(message)), .map(move |message| Message::TabMessage(message)),
); );
let mut row = widget::row::with_capacity( tab_column = tab_column.push(self.button_row());
if !self.filters.is_empty() { 1 } else { 0 } + self.choices.len() * 2 + 3,
)
.align_items(Alignment::Center)
.padding(space_xxs)
.spacing(space_xxs);
if !self.filters.is_empty() {
row = row.push(widget::dropdown(
&self.filters,
self.filter_selected,
Message::Filter,
));
}
for (choice_i, choice) in self.choices.iter().enumerate() {
match choice {
DialogChoice::CheckBox { label, value, .. } => {
row = row.push(widget::checkbox(label, *value, move |checked| {
Message::Choice(choice_i, if checked { 1 } else { 0 })
}));
}
DialogChoice::ComboBox {
label,
options,
selected,
..
} => {
row = row.push(widget::text::heading(label));
row = row.push(widget::dropdown(options, *selected, move |option_i| {
Message::Choice(choice_i, option_i)
}));
}
}
}
if let DialogKind::SaveFile { filename } = &self.flags.kind {
row = row.push(
widget::text_input("", filename)
.id(self.filename_id.clone())
.on_input(Message::Filename)
.on_submit(Message::Save(false)),
);
} else {
row = row.push(widget::horizontal_space(Length::Fill));
}
row = row.push(widget::button::standard(fl!("cancel")).on_press(Message::Cancel));
row = row.push(if self.flags.kind.save() {
widget::button::suggested(&self.accept_label).on_press(Message::Save(false))
} else {
widget::button::suggested(&self.accept_label).on_press(Message::Open)
});
tab_column = tab_column.push(row);
let content: Element<_> = tab_column.into(); let content: Element<_> = tab_column.into();

View file

@ -818,6 +818,7 @@ pub enum Command {
OpenInNewWindow(PathBuf), OpenInNewWindow(PathBuf),
Preview(PreviewKind, Duration), Preview(PreviewKind, Duration),
PreviewCancel, PreviewCancel,
WindowDrag,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@ -837,6 +838,7 @@ pub enum Message {
EditLocation(Option<Location>), EditLocation(Option<Location>),
OpenInNewTab(PathBuf), OpenInNewTab(PathBuf),
EmptyTrash, EmptyTrash,
Gallery(bool),
GoNext, GoNext,
GoPrevious, GoPrevious,
ItemDown, ItemDown,
@ -861,6 +863,7 @@ pub enum Message {
DndHover(Location), DndHover(Location),
DndEnter(Location), DndEnter(Location),
DndLeave(Location), DndLeave(Location),
WindowDrag,
ZoomDefault, ZoomDefault,
ZoomIn, ZoomIn,
ZoomOut, ZoomOut,
@ -1042,20 +1045,21 @@ impl Item {
widget::button::icon(widget::icon::from_name("go-next-symbolic")) widget::button::icon(widget::icon::from_name("go-next-symbolic"))
.on_press(app::Message::TabMessage(None, Message::ItemRight)), .on_press(app::Message::TabMessage(None, Message::ItemRight)),
); );
/*
match self match self
.thumbnail_opt .thumbnail_opt
.as_ref() .as_ref()
.unwrap_or(&ItemThumbnail::NotImage) .unwrap_or(&ItemThumbnail::NotImage)
{ {
ItemThumbnail::NotImage => {} ItemThumbnail::Rgba(_, _) => {
_ => { if let Some(path) = self.path_opt() {
row = row.push(widget::button::icon(widget::icon::from_name( row = row.push(
"window-maximize-symbolic", widget::button::icon(widget::icon::from_name("view-fullscreen-symbolic"))
))); .on_press(app::Message::TabMessage(None, Message::Gallery(true))),
);
}
} }
_ => {}
} }
*/
column = column.push(row); column = column.push(row);
column = column.push(widget::row::with_children(vec![ column = column.push(widget::row::with_children(vec![
@ -1268,6 +1272,7 @@ pub struct Tab {
pub history_i: usize, pub history_i: usize,
pub history: Vec<Location>, pub history: Vec<Location>,
pub config: TabConfig, pub config: TabConfig,
pub gallery: bool,
pub(crate) items_opt: Option<Vec<Item>>, pub(crate) items_opt: Option<Vec<Item>>,
pub dnd_hovered: Option<(Location, Instant)>, pub dnd_hovered: Option<(Location, Instant)>,
scrollable_id: widget::Id, scrollable_id: widget::Id,
@ -1315,6 +1320,7 @@ impl Tab {
history_i: 0, history_i: 0,
history, history,
config, config,
gallery: false,
items_opt: None, items_opt: None,
scrollable_id: widget::Id::unique(), scrollable_id: widget::Id::unique(),
select_focus: None, select_focus: None,
@ -1897,6 +1903,9 @@ impl Tab {
Message::EmptyTrash => { Message::EmptyTrash => {
commands.push(Command::EmptyTrash); commands.push(Command::EmptyTrash);
} }
Message::Gallery(gallery) => {
self.gallery = gallery;
}
Message::GoNext => { Message::GoNext => {
if let Some(history_i) = self.history_i.checked_add(1) { if let Some(history_i) = self.history_i.checked_add(1) {
if let Some(location) = self.history.get(history_i) { if let Some(location) = self.history.get(history_i) {
@ -2267,6 +2276,9 @@ impl Tab {
self.dnd_hovered = None; self.dnd_hovered = None;
} }
} }
Message::WindowDrag => {
commands.push(Command::WindowDrag);
}
Message::ZoomDefault => match self.config.view { Message::ZoomDefault => match self.config.view {
View::List => self.config.icon_sizes.list = 100.try_into().unwrap(), View::List => self.config.icon_sizes.list = 100.try_into().unwrap(),
View::Grid => self.config.icon_sizes.grid = 100.try_into().unwrap(), View::Grid => self.config.icon_sizes.grid = 100.try_into().unwrap(),
@ -2493,6 +2505,98 @@ impl Tab {
.into() .into()
} }
pub fn gallery_view(&self) -> Element<Message> {
let cosmic_theme::Spacing {
space_xxs,
space_m,
space_l,
..
} = theme::active().cosmic().spacing;
//TODO: display error messages when image not found?
let mut name_opt = None;
let mut image_opt = None;
if let Some(index) = self.select_focus {
if let Some(items) = &self.items_opt {
if let Some(item) = items.get(index) {
name_opt = Some(widget::text::heading(&item.display_name));
match item
.thumbnail_opt
.as_ref()
.unwrap_or(&ItemThumbnail::NotImage)
{
ItemThumbnail::Rgba(_, _) => {
if let Some(path) = item.path_opt() {
image_opt = Some(
widget::image::viewer(widget::image::Handle::from_path(path))
.min_scale(1.0)
.width(Length::Fill)
.height(Length::Fill),
);
}
}
_ => {}
}
}
}
}
let mut column = widget::column::with_capacity(2);
column = column.push(widget::vertical_space(Length::Fixed(space_xxs.into())));
{
let mut row = widget::row::with_capacity(5).align_items(Alignment::Center);
row = row.push(widget::horizontal_space(Length::Fill));
if let Some(name) = name_opt {
row = row.push(name);
}
row = row.push(widget::horizontal_space(Length::Fill));
row = row.push(
widget::button::icon(widget::icon::from_name("window-close-symbolic"))
.on_press(Message::Gallery(false)),
);
row = row.push(widget::horizontal_space(Length::Fixed(space_xxs.into())));
// This mouse area provides window drag while the header bar is hidden
let mouse_area = mouse_area::MouseArea::new(row).on_press(|_| Message::WindowDrag);
column = column.push(mouse_area);
}
{
let mut row = widget::row::with_capacity(7).align_items(Alignment::Center);
row = row.push(widget::horizontal_space(Length::Fixed(space_m.into())));
row = row.push(
widget::button::icon(widget::icon::from_name("go-previous-symbolic"))
.on_press(Message::ItemLeft),
);
row = row.push(widget::horizontal_space(Length::Fixed(space_xxs.into())));
if let Some(image) = image_opt {
row = row.push(image);
} else {
//TODO: what to do when no image?
row = row.push(widget::horizontal_space(Length::Fill));
}
row = row.push(widget::horizontal_space(Length::Fixed(space_xxs.into())));
row = row.push(
widget::button::icon(widget::icon::from_name("go-next-symbolic"))
.on_press(Message::ItemRight),
);
row = row.push(widget::horizontal_space(Length::Fixed(space_m.into())));
column = column.push(row);
}
widget::container(column)
.width(Length::Fill)
.height(Length::Fill)
.style(theme::Container::Custom(Box::new(|theme| {
let cosmic = theme.cosmic();
let mut bg = cosmic.bg_color();
bg.alpha = 0.75;
widget::container::Appearance {
background: Some(Color::from(bg).into()),
..Default::default()
}
})))
.into()
}
pub fn location_view(&self) -> Element<Message> { pub fn location_view(&self) -> Element<Message> {
//TODO: responsiveness is done in a hacky way, potentially move this to a custom widget? //TODO: responsiveness is done in a hacky way, potentially move this to a custom widget?
fn text_width<'a>( fn text_width<'a>(
@ -2590,7 +2694,7 @@ impl Tab {
_ => {} _ => {}
} }
//TODO: make it possible to resize with the mouse //TODO: make it possible to resize with the mouse
return crate::mouse_area::MouseArea::new(row) return mouse_area::MouseArea::new(row)
.on_press(move |_point_opt| Message::ToggleSort(msg)) .on_press(move |_point_opt| Message::ToggleSort(msg))
.into(); .into();
}; };