general: Add fullscreen widget

This commit is contained in:
Christopher Davis 2024-06-13 00:24:45 -04:00
parent 422750949c
commit 230ffd5c77
6 changed files with 264 additions and 115 deletions

View file

@ -22,6 +22,8 @@ src/widgets.rs
src/widgets/drag_overlay.rs src/widgets/drag_overlay.rs
src/widgets/error_details.rs src/widgets/error_details.rs
src/widgets/error_details.ui src/widgets/error_details.ui
src/widgets/fullscreen_widget.rs
src/widgets/fullscreen_widget.ui
src/widgets/image.rs src/widgets/image.rs
src/widgets/image/background_color.rs src/widgets/image/background_color.rs
src/widgets/image/drag.rs src/widgets/image/drag.rs

View file

@ -19,6 +19,7 @@
mod drag_overlay; mod drag_overlay;
mod error_details; mod error_details;
mod fullscreen_widget;
mod image; mod image;
mod image_page; mod image_page;
mod image_view; mod image_view;
@ -30,6 +31,7 @@ mod sliding_view;
pub use drag_overlay::LpDragOverlay; pub use drag_overlay::LpDragOverlay;
pub use error_details::LpErrorDetails; pub use error_details::LpErrorDetails;
pub use fullscreen_widget::LpFullscreenWidget;
pub use image::LpImage; pub use image::LpImage;
pub use image_page::LpImagePage; pub use image_page::LpImagePage;
pub use image_view::LpImageView; pub use image_view::LpImageView;

View file

@ -0,0 +1,147 @@
// Copyright (c) 2024 Christopher Davis
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// SPDX-License-Identifier: GPL-3.0-or-later
use std::cell::{Cell, RefCell};
use adw::prelude::*;
use adw::subclass::prelude::*;
use glib::Properties;
use gtk::CompositeTemplate;
use super::*;
use crate::deps::*;
mod imp {
use super::*;
#[derive(Default, Debug, Properties, CompositeTemplate)]
#[properties(wrapper_type = super::LpFullscreenWidget)]
#[template(file = "fullscreen_widget.ui")]
pub struct LpFullscreenWidget {
#[template_child]
pub(super) toolbar_view: TemplateChild<adw::ToolbarView>,
#[template_child]
pub(super) header_container: TemplateChild<LpShyBin>,
#[property(get, set, nullable)]
headerbar: RefCell<Option<gtk::Widget>>,
#[property(get, set, nullable)]
content: RefCell<Option<gtk::Widget>>,
#[property(get, set)]
fullscreened: Cell<bool>,
#[property(get, set)]
extend_content: Cell<bool>,
#[property(get = LpFullscreenWidget::is_content_extended_to_top, type = bool)]
_is_content_extended_to_top: (),
#[property(get, set)]
is_empty: Cell<bool>,
#[property(get, set)]
headerbar_opacity: Cell<f64>,
}
impl LpFullscreenWidget {
fn is_content_extended_to_top(&self) -> bool {
let obj = self.obj();
obj.fullscreened() && obj.extend_content()
}
}
#[glib::object_subclass]
impl ObjectSubclass for LpFullscreenWidget {
const NAME: &'static str = "LpFullscreenWidget";
type Type = super::LpFullscreenWidget;
type ParentType = adw::Bin;
fn class_init(klass: &mut Self::Class) {
klass.bind_template();
}
fn instance_init(obj: &gio::glib::subclass::InitializingObject<Self>) {
obj.init_template();
}
}
#[glib::derived_properties]
impl ObjectImpl for LpFullscreenWidget {
fn constructed(&self) {
self.parent_constructed();
let obj = self.obj();
obj.connect_is_empty_notify(|obj| {
obj.update_headerbar_style();
});
obj.connect_fullscreened_notify(|obj| {
obj.update_headerbar_style();
});
obj.connect_extend_content_notify(|obj| {
obj.update_headerbar_style();
});
}
}
impl WidgetImpl for LpFullscreenWidget {}
impl BinImpl for LpFullscreenWidget {}
}
glib::wrapper! {
pub struct LpFullscreenWidget(ObjectSubclass<imp::LpFullscreenWidget>)
@extends gtk::Widget, adw::Bin,
@implements gtk::Buildable, gtk::ConstraintTarget;
}
impl LpFullscreenWidget {
pub fn is_headerbar_flat(&self) -> bool {
matches!(
self.imp().toolbar_view.top_bar_style(),
adw::ToolbarStyle::Flat
)
}
fn update_header_opacity(&self) {
if let Some(headerbar) = self.headerbar() {
if self.is_headerbar_flat() && !self.is_empty() {
// Bring headerbar opacity in sync with controls
headerbar.set_opacity(self.headerbar_opacity());
} else {
headerbar.set_opacity(1.);
};
}
}
fn update_headerbar_style(&self) {
self.imp()
.toolbar_view
.set_extend_content_to_top_edge(self.is_content_extended_to_top());
// Flat headerbar for empty state and fullscreen
let style = if self.is_empty() || self.is_content_extended_to_top() {
adw::ToolbarStyle::Flat
} else {
// Use the border variant of raised to avoid shadows over images
adw::ToolbarStyle::RaisedBorder
};
let toolbar_view = self.imp().toolbar_view.get();
if style != toolbar_view.top_bar_style() {
toolbar_view.set_top_bar_style(style);
self.update_header_opacity();
}
}
}

View file

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="LpFullscreenWidget" parent="AdwBin">
<binding name="fullscreened">
<lookup name="fullscreened" type="GtkWindow">
<lookup name="root">LpFullscreenWidget</lookup>
</lookup>
</binding>
<binding name="is-empty">
<lookup name="is-empty" type="LpWindow">
<lookup name="root">LpFullscreenWidget</lookup>
</lookup>
</binding>
<binding name="headerbar-opacity">
<lookup name="headerbar-opacity" type="LpWindow">
<lookup name="root">LpFullscreenWidget</lookup>
</lookup>
</binding>
<property name="child">
<object class="AdwToolbarView" id="toolbar_view">
<child type="top">
<object class="LpShyBin" id="header_container">
<property name="child" bind-source="LpFullscreenWidget" bind-property="headerbar"/>
</object>
</child>
<property name="content" bind-source="LpFullscreenWidget" bind-property="content"/>
</object>
</property>
</template>
</interface>

View file

@ -34,7 +34,7 @@ use std::path::{Path, PathBuf};
use actions::*; use actions::*;
use adw::prelude::*; use adw::prelude::*;
use adw::subclass::prelude::*; use adw::subclass::prelude::*;
use glib::clone; use glib::{clone, Properties};
use gtk::{CompositeTemplate, Widget}; use gtk::{CompositeTemplate, Widget};
use crate::application::LpApplication; use crate::application::LpApplication;
@ -42,7 +42,7 @@ use crate::config;
use crate::deps::*; use crate::deps::*;
use crate::util::gettext::*; use crate::util::gettext::*;
use crate::util::Direction; use crate::util::Direction;
use crate::widgets::{LpDragOverlay, LpImage, LpImageView, LpPropertiesView}; use crate::widgets::{LpDragOverlay, LpFullscreenWidget, LpImage, LpImageView, LpPropertiesView};
/// Show window after X milliseconds even if image dimensions are not known yet /// Show window after X milliseconds even if image dimensions are not known yet
const SHOW_WINDOW_AFTER: u64 = 2000; const SHOW_WINDOW_AFTER: u64 = 2000;
@ -73,14 +73,15 @@ mod imp {
// If some member fields did not implement default, // If some member fields did not implement default,
// we'd need to have a `new()` function in the // we'd need to have a `new()` function in the
// `impl ObjectSubclass for $TYPE` section. // `impl ObjectSubclass for $TYPE` section.
#[derive(Default, Debug, CompositeTemplate)] #[derive(Default, Debug, Properties, CompositeTemplate)]
#[properties(wrapper_type = super::LpWindow)]
#[template(file = "window.ui")] #[template(file = "window.ui")]
pub struct LpWindow { pub struct LpWindow {
// Template children are used with the // Template children are used with the
// TemplateChild<T> wrapper, where T is the // TemplateChild<T> wrapper, where T is the
// object type of the template child. // object type of the template child.
#[template_child] #[template_child]
pub(super) toolbar_view: TemplateChild<adw::ToolbarView>, pub(super) fullscreen_widget: TemplateChild<LpFullscreenWidget>,
#[template_child] #[template_child]
pub(super) headerbar: TemplateChild<adw::HeaderBar>, pub(super) headerbar: TemplateChild<adw::HeaderBar>,
#[template_child] #[template_child]
@ -110,6 +111,11 @@ mod imp {
#[template_child] #[template_child]
pub(super) backward_click_gesture: TemplateChild<gtk::GestureClick>, pub(super) backward_click_gesture: TemplateChild<gtk::GestureClick>,
#[property(get, set)]
is_empty: Cell<bool>,
#[property(get, set)]
headerbar_opacity: Cell<f64>,
/// Motion controller for complete window /// Motion controller for complete window
pub(super) motion_controller: gtk::EventControllerMotion, pub(super) motion_controller: gtk::EventControllerMotion,
pub(super) pointer_position: Cell<(f64, f64)>, pub(super) pointer_position: Cell<(f64, f64)>,
@ -167,6 +173,7 @@ mod imp {
} }
} }
#[glib::derived_properties]
impl ObjectImpl for LpWindow { impl ObjectImpl for LpWindow {
fn constructed(&self) { fn constructed(&self) {
self.parent_constructed(); self.parent_constructed();
@ -233,12 +240,6 @@ mod imp {
win.resize_default(); win.resize_default();
}); });
self.properties_button.connect_active_notify(glib::clone!(
#[weak]
obj,
move |_| obj.update_headerbar_style()
));
let gesture_click = gtk::GestureClick::new(); let gesture_click = gtk::GestureClick::new();
gesture_click.connect_pressed(glib::clone!( gesture_click.connect_pressed(glib::clone!(
#[weak] #[weak]
@ -339,11 +340,11 @@ mod imp {
), ),
); );
self.toolbar_view.connect_top_bar_style_notify(glib::clone!( self.image_view
#[weak] .controls_box_start()
obj, .bind_property("opacity", &*self.obj(), "headerbar-opacity")
move |_| obj.on_top_bar_style_notify() .sync_create()
)); .build();
// action win.previous status // action win.previous status
self.image_view self.image_view
@ -760,7 +761,7 @@ impl LpWindow {
let current_page = imp.image_view.current_page(); let current_page = imp.image_view.current_page();
// HeaderBar style // HeaderBar style
self.update_headerbar_style(); self.set_is_empty(current_page.is_none());
// Window title // Window title
self.update_title(); self.update_title();
@ -896,8 +897,6 @@ impl LpWindow {
.image_view .image_view
.on_fullscreen_changed(self.is_fullscreen()); .on_fullscreen_changed(self.is_fullscreen());
self.update_headerbar_style();
if !self.is_fullscreen() { if !self.is_fullscreen() {
self.set_cursor(None); self.set_cursor(None);
self.show_controls(); self.show_controls();
@ -906,39 +905,11 @@ impl LpWindow {
} }
fn is_headerbar_flat(&self) -> bool { fn is_headerbar_flat(&self) -> bool {
matches!( self.imp().fullscreen_widget.is_headerbar_flat()
self.imp().toolbar_view.top_bar_style(),
adw::ToolbarStyle::Flat
)
}
fn on_top_bar_style_notify(&self) {
if self.is_headerbar_flat() {
// Bring headerbar opacity in sync with controls
self.headerbar().set_opacity(self.controls_opacity());
} else {
self.headerbar().set_opacity(1.);
}
} }
fn is_content_extended_to_top(&self) -> bool { fn is_content_extended_to_top(&self) -> bool {
self.is_fullscreen() && !self.imp().properties_button.is_active() self.imp().fullscreen_widget.is_content_extended_to_top()
}
fn update_headerbar_style(&self) {
let imp = self.imp();
let extend = self.is_content_extended_to_top();
imp.toolbar_view.set_extend_content_to_top_edge(extend);
// Flat headerbar for empty state and fullscreen
let toolbar_style = if imp.image_view.current_image().is_none() || extend {
adw::ToolbarStyle::Flat
} else {
// Use the border variant of raised to avoid shadows over images
adw::ToolbarStyle::RaisedBorder
};
imp.toolbar_view.set_top_bar_style(toolbar_style);
} }
fn update_accel_status(&self) { fn update_accel_status(&self) {

View file

@ -68,75 +68,72 @@
</object> </object>
</child> </child>
<property name="content"> <property name="content">
<object class="AdwToolbarView" id="toolbar_view"> <object class="LpFullscreenWidget" id="fullscreen_widget">
<child type="top"> <property name="extend-content" bind-source="properties_button" bind-property="active" bind-flags="sync-create|invert-boolean"/>
<object class="LpShyBin"> <property name="headerbar">
<property name="child"> <object class="AdwHeaderBar" id="headerbar">
<object class="AdwHeaderBar" id="headerbar"> <property name="show-end-title-buttons" bind-source="LpWindow" bind-property="fullscreened" bind-flags="sync-create|invert-boolean" />
<property name="show-end-title-buttons" bind-source="LpWindow" bind-property="fullscreened" bind-flags="sync-create|invert-boolean" /> <property name="show-start-title-buttons" bind-source="LpWindow" bind-property="fullscreened" bind-flags="sync-create|invert-boolean" />
<property name="show-start-title-buttons" bind-source="LpWindow" bind-property="fullscreened" bind-flags="sync-create|invert-boolean" /> <style>
<style> <class name="fullscreen-osd" />
<class name="fullscreen-osd" /> </style>
</style> <child type="start">
<child type="start"> <object class="GtkButton">
<object class="GtkButton"> <property name="icon-name">edit-copy-symbolic</property>
<property name="icon-name">edit-copy-symbolic</property> <property name="action-name">win.copy-image</property>
<property name="action-name">win.copy-image</property> <property name="tooltip-text" translatable="yes">Copy to Clipboard</property>
<property name="tooltip-text" translatable="yes">Copy to Clipboard</property>
</object>
</child>
<child type="start">
<object class="GtkButton">
<property name="icon-name">user-trash-symbolic</property>
<property name="action-name">win.trash</property>
<property name="tooltip-text" translatable="yes">Move to Trash</property>
</object>
</child>
<child type="end">
<object class="GtkMenuButton" id="primary_menu">
<property name="icon-name">open-menu-symbolic</property>
<property name="tooltip-text" translatable="yes">Main Menu</property>
<property name="primary">True</property>
<property name="popover">
<object class="GtkPopoverMenu">
<property name="menu-model">primary_menu</property>
<child type="rotate-left">
<object class="GtkButton">
<property name="icon-name">object-rotate-left-symbolic</property>
<property name="tooltip-text" translatable="yes">Rotate Left</property>
<property name="action-name">win.rotate-ccw</property>
<style>
<class name="flat" />
</style>
</object>
</child>
<child type="rotate-right">
<object class="GtkButton">
<property name="icon-name">object-rotate-right-symbolic</property>
<property name="tooltip-text" translatable="yes">Rotate Right</property>
<property name="action-name">win.rotate-cw</property>
<style>
<class name="flat" />
</style>
</object>
</child>
</object>
</property>
</object>
</child>
<child type="end">
<object class="GtkToggleButton" id="properties_button">
<property name="icon-name">info-outline-symbolic</property>
<property name="tooltip-text" translatable="yes">Image Properties</property>
</object>
</child>
<child>
<object class="GtkEventControllerMotion" id="headerbar_events" />
</child>
</object> </object>
</property> </child>
<child type="start">
<object class="GtkButton">
<property name="icon-name">user-trash-symbolic</property>
<property name="action-name">win.trash</property>
<property name="tooltip-text" translatable="yes">Move to Trash</property>
</object>
</child>
<child type="end">
<object class="GtkMenuButton" id="primary_menu">
<property name="icon-name">open-menu-symbolic</property>
<property name="tooltip-text" translatable="yes">Main Menu</property>
<property name="primary">True</property>
<property name="popover">
<object class="GtkPopoverMenu">
<property name="menu-model">primary_menu</property>
<child type="rotate-left">
<object class="GtkButton">
<property name="icon-name">object-rotate-left-symbolic</property>
<property name="tooltip-text" translatable="yes">Rotate Left</property>
<property name="action-name">win.rotate-ccw</property>
<style>
<class name="flat" />
</style>
</object>
</child>
<child type="rotate-right">
<object class="GtkButton">
<property name="icon-name">object-rotate-right-symbolic</property>
<property name="tooltip-text" translatable="yes">Rotate Right</property>
<property name="action-name">win.rotate-cw</property>
<style>
<class name="flat" />
</style>
</object>
</child>
</object>
</property>
</object>
</child>
<child type="end">
<object class="GtkToggleButton" id="properties_button">
<property name="icon-name">info-outline-symbolic</property>
<property name="tooltip-text" translatable="yes">Image Properties</property>
</object>
</child>
<child>
<object class="GtkEventControllerMotion" id="headerbar_events" />
</child>
</object> </object>
</child> </property>
<property name="content"> <property name="content">
<object class="AdwToastOverlay" id="toast_overlay"> <object class="AdwToastOverlay" id="toast_overlay">
<property name="child"> <property name="child">