Gallery view (#488)
* WIP: gallery view * Adjust gallery view to design * Update dialog to better match gallery design
This commit is contained in:
parent
eda1189f08
commit
7b2e448947
3 changed files with 216 additions and 61 deletions
22
src/app.rs
22
src/app.rs
|
|
@ -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,
|
||||||
|
|
|
||||||
135
src/dialog.rs
135
src/dialog.rs
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
||||||
120
src/tab.rs
120
src/tab.rs
|
|
@ -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();
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue