flare: update image patch

This commit is contained in:
2026-05-12 12:12:22 -04:00
parent 9417716e68
commit 9afcb8f892
3 changed files with 56 additions and 554 deletions

View File

@@ -89,7 +89,7 @@
# alternative GTK signal client; carries local feature patches under # alternative GTK signal client; carries local feature patches under
# patches/flare/ on top of upstream master (typing indicators, edited # patches/flare/ on top of upstream master (typing indicators, edited
# messages, multi-select with delete-for-me, in-channel message search, # messages, multi-select with delete-for-me, in-channel message search,
# the deleted-message placeholder, the image-viewer overlay, and the # the deleted-message placeholder, the image-viewer dialog, and the
# not-yet-merged init_channels cache-miss fix). # not-yet-merged init_channels cache-miss fix).
(pkgs.flare-signal.overrideAttrs (old: { (pkgs.flare-signal.overrideAttrs (old: {
src = inputs.flare-upstream; src = inputs.flare-upstream;
@@ -106,15 +106,6 @@
../../patches/flare/0006-fix-backend-Refresh-cached-channels-on-init_channels.patch ../../patches/flare/0006-fix-backend-Refresh-cached-channels-on-init_channels.patch
../../patches/flare/0007-feat-messages-Open-image-attachments-in-a-fullscreen.patch ../../patches/flare/0007-feat-messages-Open-image-attachments-in-a-fullscreen.patch
]; ];
# The image-viewer demo patch (0007) references
# data/screenshots/dummy_apple.jpg, but git binary diffs are not
# supported by the standard `patch` tool nixpkgs uses for the
# patchPhase. Ship the JPEG alongside the patches and copy it into
# the source tree before the build picks it up.
postPatch = (old.postPatch or "") + ''
install -m 0644 ${./../../patches/flare/dummy_apple.jpg} \
data/screenshots/dummy_apple.jpg
'';
})) }))
# accounting # accounting

View File

@@ -1,60 +1,42 @@
From e13d8cbf15a2eea323397accb2183e2362dae106 Mon Sep 17 00:00:00 2001 From c185703eef79a48dead219f6b41a7874994c7273 Mon Sep 17 00:00:00 2001
From: Simon Gardling <titaniumtown@proton.me> From: Simon Gardling <titaniumtown@proton.me>
Date: Wed, 6 May 2026 18:41:15 -0400 Date: Tue, 12 May 2026 12:03:33 -0400
Subject: [PATCH 7/7] feat(messages): Open image attachments in a Subject: [PATCH] feat(messages): Open image attachments in a dialog
fullscreen-capable viewer
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Clicking an image bubble currently does nothing the existing Mirrors the official Signal desktop application by allowing the user
"pressed" gesture is right-click-only and routes to the context menu. to click on an image attachment and see it expanded in an Adw.Dialog
To actually look at an image at full resolution the user has to overlay with built-in close button, Escape handling, and mobile
right-click and choose "Open", which dispatches the file through the swipe-down support.
xdg-open portal to the system image viewer. That is the wrong
affordance for a chat client; every other Signal frontend opens the
image inline on tap.
Add an `ImageViewer` (Adw.Window subclass) that hosts the attachment's
texture in a `gtk::Picture` filling the window over a near-black
backdrop, with two `osd circular` buttons floating in the top-right
corner: a fullscreen toggle and a close button. Closing happens on
`Escape` or via the close button — clicks on the picture and on the
letterboxed area do nothing, leaving the gesture stack open for a
future pan/zoom implementation.
Wire `AttachmentImage` to open the viewer on left-click. Bails out
silently if the texture has not loaded yet or if the row is not
currently parented in a `gtk::Window`.
--- ---
CHANGELOG.md | 1 + CHANGELOG.md | 4 ++
data/resources/meson.build | 1 + data/resources/meson.build | 1 +
data/resources/resources.gresource.xml.in | 5 + data/resources/resources.gresource.xml.in | 1 +
data/resources/style.css | 8 +- .../ui/components/attachment_image.blp | 5 ++
.../ui/components/attachment_image.blp | 5 + data/resources/ui/image_viewer.blp | 15 +++++
data/resources/ui/image_viewer.blp | 64 +++++ src/backend/dummy.rs | 3 +-
data/resources/ui/window.blp | 224 +++++++++--------- src/gui/components/attachment_image.rs | 28 ++++++++-
src/backend/dummy.rs | 36 ++- src/gui/image_viewer.rs | 57 +++++++++++++++++++
src/gui/components/attachment_image.rs | 33 ++-
src/gui/image_viewer.rs | 120 ++++++++++
src/gui/mod.rs | 1 + src/gui/mod.rs | 1 +
src/gui/window.rs | 30 +++ src/gui/window.rs | 7 +++
13 files changed, 411 insertions(+), 117 deletions(-) 10 files changed, 120 insertions(+), 2 deletions(-)
create mode 100644 data/resources/ui/image_viewer.blp create mode 100644 data/resources/ui/image_viewer.blp
create mode 100644 src/gui/image_viewer.rs create mode 100644 src/gui/image_viewer.rs
diff --git a/CHANGELOG.md b/CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md
index e221f555..8ebca71c 100644 index 20dc578e..1e4649bc 100644
--- a/CHANGELOG.md --- a/CHANGELOG.md
+++ b/CHANGELOG.md +++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Display incoming edited messages with an `edited` indicator and edit your own sent messages from their context menu.
- Multi-select messages from their context menu and delete the selection locally with a single action.
- In-channel message search (Ctrl+Shift+F) over the loaded timeline with prev/next navigation and a match counter.
+- Click an image attachment to open it in a fullscreen-capable viewer with a close button, fullscreen toggle, and `Escape`-to-dismiss.
## [Unreleased]
+### Added
+
+- Click an image attachment to open it in a fullscreen-capable viewer with a close button, fullscreen toggle, and `Escape`-to-dismiss.
+
## [0.20.4] - 2026-04-22 ## [0.20.4] - 2026-04-22
### Fixed
diff --git a/data/resources/meson.build b/data/resources/meson.build diff --git a/data/resources/meson.build b/data/resources/meson.build
index 7fc00b7c..efd789b7 100644 index 7fc00b7c..efd789b7 100644
--- a/data/resources/meson.build --- a/data/resources/meson.build
@@ -68,21 +50,10 @@ index 7fc00b7c..efd789b7 100644
'ui/linked_devices_window.blp', 'ui/linked_devices_window.blp',
'ui/device_info_item.blp', 'ui/device_info_item.blp',
diff --git a/data/resources/resources.gresource.xml.in b/data/resources/resources.gresource.xml.in diff --git a/data/resources/resources.gresource.xml.in b/data/resources/resources.gresource.xml.in
index 8604fe24..863564b3 100644 index 8604fe24..7c958d02 100644
--- a/data/resources/resources.gresource.xml.in --- a/data/resources/resources.gresource.xml.in
+++ b/data/resources/resources.gresource.xml.in +++ b/data/resources/resources.gresource.xml.in
@@ -2,6 +2,10 @@ @@ -13,6 +13,7 @@
<gresources>
<gresource prefix="/">
<file alias="icon.svg">../icons/de.schmidhuberj.Flare.svg</file>
+ <!-- Photo bundled into the screenshot-mode build only as the demo image-->
+ <!-- attachment in `src/backend/dummy.rs`. "Big red apple.jpg" by-->
+ <!-- Paolo Neo, public domain, from Wikimedia Commons.-->
+ <file alias="dummy_apple.jpg">../screenshots/dummy_apple.jpg</file>
<file preprocess="xml-stripblanks">ui/window.ui</file>
<file preprocess="xml-stripblanks">ui/channel_list.ui</file>
@@ -13,6 +17,7 @@
<file preprocess="xml-stripblanks">ui/linked_devices_window.ui</file> <file preprocess="xml-stripblanks">ui/linked_devices_window.ui</file>
<file preprocess="xml-stripblanks">ui/device_info_item.ui</file> <file preprocess="xml-stripblanks">ui/device_info_item.ui</file>
<file preprocess="xml-stripblanks">ui/error_dialog.ui</file> <file preprocess="xml-stripblanks">ui/error_dialog.ui</file>
@@ -90,23 +61,6 @@ index 8604fe24..863564b3 100644
<file preprocess="xml-stripblanks">ui/attachment.ui</file> <file preprocess="xml-stripblanks">ui/attachment.ui</file>
<file preprocess="xml-stripblanks">ui/preferences_window.ui</file> <file preprocess="xml-stripblanks">ui/preferences_window.ui</file>
<file preprocess="xml-stripblanks">ui/shortcuts.ui</file> <file preprocess="xml-stripblanks">ui/shortcuts.ui</file>
diff --git a/data/resources/style.css b/data/resources/style.css
index b3a517f9..0c95a55a 100644
--- a/data/resources/style.css
+++ b/data/resources/style.css
@@ -309,4 +309,10 @@ unread-indicator {
background: alpha(@accent_bg_color, 0.35);
outline: 2px solid alpha(@accent_color, 0.7);
outline-offset: -2px;
-}
\ No newline at end of file
+}
+
+/* Image attachment viewer: a near-black backdrop so the picture appears to
+ float and the floating OSD close button has enough contrast. */
+.image-viewer-backdrop {
+ background-color: rgb(15, 15, 15);
+}
diff --git a/data/resources/ui/components/attachment_image.blp b/data/resources/ui/components/attachment_image.blp diff --git a/data/resources/ui/components/attachment_image.blp b/data/resources/ui/components/attachment_image.blp
index ec303a72..bcbdabe1 100644 index ec303a72..bcbdabe1 100644
--- a/data/resources/ui/components/attachment_image.blp --- a/data/resources/ui/components/attachment_image.blp
@@ -125,44 +79,16 @@ index ec303a72..bcbdabe1 100644
] ]
diff --git a/data/resources/ui/image_viewer.blp b/data/resources/ui/image_viewer.blp diff --git a/data/resources/ui/image_viewer.blp b/data/resources/ui/image_viewer.blp
new file mode 100644 new file mode 100644
index 00000000..1b27cc09 index 00000000..19660f0b
--- /dev/null --- /dev/null
+++ b/data/resources/ui/image_viewer.blp +++ b/data/resources/ui/image_viewer.blp
@@ -0,0 +1,64 @@ @@ -0,0 +1,15 @@
+using Gtk 4.0; +using Gtk 4.0;
+using Adw 1; +using Adw 1;
+ +
+template $FlImageViewer: Adw.Bin { +template $FlImageViewer: Adw.Bin {
+ hexpand: true; + hexpand: true;
+ vexpand: true; + vexpand: true;
+ focusable: true;
+
+ EventControllerKey {
+ key-pressed => $on_key_pressed() swapped;
+ }
+
+ // Box paints the dark backdrop. Overlay does not draw a background, so
+ // the CSS class lives on the Box.
+ Box {
+ hexpand: true;
+ vexpand: true;
+
+ styles [
+ "image-viewer-backdrop",
+ ]
+
+ Overlay {
+ hexpand: true;
+ vexpand: true;
+
+ // Any left-click in the viewer dismisses — on the image, on the
+ // letterboxed dark area, anywhere. The close button absorbs its own
+ // click before it reaches us via standard GTK click-bubbling so the
+ // button still works.
+ GestureClick {
+ button: 1;
+ pressed => $on_backdrop_pressed() swapped;
+ }
+ +
+ Picture picture { + Picture picture {
+ paintable: bind template.attachment as <$FlAttachmentObject>.image; + paintable: bind template.attachment as <$FlAttachmentObject>.image;
@@ -171,340 +97,30 @@ index 00000000..1b27cc09
+ hexpand: true; + hexpand: true;
+ vexpand: true; + vexpand: true;
+ } + }
+
+ [overlay]
+ Box {
+ halign: end;
+ valign: start;
+ margin-top: 12;
+ margin-end: 12;
+
+ Button close_button {
+ icon-name: "window-close-symbolic";
+ tooltip-text: _("Close");
+ clicked => $on_close_clicked() swapped;
+
+ styles [
+ "osd",
+ "circular",
+ ]
+ }
+ }
+ }
+ }
+} +}
diff --git a/data/resources/ui/window.blp b/data/resources/ui/window.blp
index 82de6af3..3706121a 100644
--- a/data/resources/ui/window.blp
+++ b/data/resources/ui/window.blp
@@ -52,130 +52,136 @@ template $FlWindow: Adw.ApplicationWindow {
Gtk.StackPage {
name: "main";
- child: Adw.NavigationSplitView split_view {
- min-sidebar-width: 300;
- max-sidebar-width: 400;
- sidebar-width-fraction: 0.25;
- notify::show-content => $handle_show_content() swapped;
-
- sidebar: Adw.NavigationPage {
- title: _("Channel list");
- tag: "channel-list";
-
- Adw.ToolbarView {
- [top]
- Adw.HeaderBar {
- title-widget: Adw.WindowTitle {
- title: "Flare";
- };
+ child: Gtk.Overlay image_viewer_overlay {
+ // Wraps the navigation split view so an image viewer pushed in via
+ // `Window::present_image_viewer` covers the entire window content
+ // (sidebar + chat) rather than being constrained to the split
+ // view's content pane like an Adw.Dialog would be.
+ Adw.NavigationSplitView split_view {
+ min-sidebar-width: 300;
+ max-sidebar-width: 400;
+ sidebar-width-fraction: 0.25;
+ notify::show-content => $handle_show_content() swapped;
+
+ sidebar: Adw.NavigationPage {
+ title: _("Channel list");
+ tag: "channel-list";
+
+ Adw.ToolbarView {
+ [top]
+ Adw.HeaderBar {
+ title-widget: Adw.WindowTitle {
+ title: "Flare";
+ };
+
+ [start]
+ Gtk.Button {
+ accessibility {
+ label: C_("accessibility", "Add Conversation");
+ }
- [start]
- Gtk.Button {
- accessibility {
- label: C_("accessibility", "Add Conversation");
+ tooltip-text: "Add Conversation";
+ icon-name: "chat-message-new-symbolic";
+ clicked => $handle_add_conversation_clicked() swapped;
}
- tooltip-text: "Add Conversation";
- icon-name: "chat-message-new-symbolic";
- clicked => $handle_add_conversation_clicked() swapped;
- }
+ [end]
+ MenuButton {
+ accessibility {
+ label: C_("accessibility", "Menu");
+ }
- [end]
- MenuButton {
- accessibility {
- label: C_("accessibility", "Menu");
+ tooltip-text: "Menu";
+ menu-model: menubar;
+ primary: true;
+ icon-name: "open-menu-symbolic";
}
- tooltip-text: "Menu";
- menu-model: menubar;
- primary: true;
- icon-name: "open-menu-symbolic";
- }
+ [end]
+ ToggleButton {
+ accessibility {
+ label: C_("accessibility", "Search");
+ }
- [end]
- ToggleButton {
- accessibility {
- label: C_("accessibility", "Search");
+ tooltip-text: "Search";
+ icon-name: "system-search-symbolic";
+ active: bind channel_list.search-enabled no-sync-create;
+ clicked => $handle_search_clicked() swapped;
}
-
- tooltip-text: "Search";
- icon-name: "system-search-symbolic";
- active: bind channel_list.search-enabled no-sync-create;
- clicked => $handle_search_clicked() swapped;
}
- }
- content: $FlChannelList channel_list {
- notify::active-channel => $handle_go_forward() swapped;
- manager: bind template.manager;
- };
- }
- };
-
- content: Adw.NavigationPage {
- title: _("Chat");
- tag: "chat";
-
- Adw.ToolbarView {
- [top]
- Adw.HeaderBar {
- title-widget: Button {
- styles [
- "flat",
- ]
-
- sensitive: bind $is_some(channel_list.active-channel) as <bool>;
- visible: bind $is_some(channel_list.active-channel) as <bool>;
- action-name: "win.channel-information";
-
- child: Box {
- name: "room_title";
- orientation: vertical;
- halign: center;
- valign: center;
-
- Label title_label {
- focusable: true;
- ellipsize: end;
- halign: center;
- wrap: false;
- single-line-mode: true;
- use-markup: false;
- width-chars: 5;
- label: bind channel_list.active-channel as <$FlChannel>.title;
-
- styles [
- "title",
- ]
- }
+ content: $FlChannelList channel_list {
+ notify::active-channel => $handle_go_forward() swapped;
+ manager: bind template.manager;
+ };
+ }
+ };
- Label subtitle_label {
- focusable: true;
- ellipsize: end;
+ content: Adw.NavigationPage {
+ title: _("Chat");
+ tag: "chat";
+
+ Adw.ToolbarView {
+ [top]
+ Adw.HeaderBar {
+ title-widget: Button {
+ styles [
+ "flat",
+ ]
+
+ sensitive: bind $is_some(channel_list.active-channel) as <bool>;
+ visible: bind $is_some(channel_list.active-channel) as <bool>;
+ action-name: "win.channel-information";
+
+ child: Box {
+ name: "room_title";
+ orientation: vertical;
halign: center;
- wrap: false;
- single-line-mode: true;
- use-markup: true;
- visible: bind channel_list.active-channel as <$FlChannel>.is-typing;
- label: bind channel_list.active-channel as <$FlChannel>.typing-label;
- tooltip-markup: bind channel_list.active-channel as <$FlChannel>.description;
-
- styles [
- "subtitle-room",
- "accent",
- ]
- }
+ valign: center;
+
+ Label title_label {
+ focusable: true;
+ ellipsize: end;
+ halign: center;
+ wrap: false;
+ single-line-mode: true;
+ use-markup: false;
+ width-chars: 5;
+ label: bind channel_list.active-channel as <$FlChannel>.title;
+
+ styles [
+ "title",
+ ]
+ }
+
+ Label subtitle_label {
+ focusable: true;
+ ellipsize: end;
+ halign: center;
+ wrap: false;
+ single-line-mode: true;
+ use-markup: true;
+ visible: bind channel_list.active-channel as <$FlChannel>.is-typing;
+ label: bind channel_list.active-channel as <$FlChannel>.typing-label;
+ tooltip-markup: bind channel_list.active-channel as <$FlChannel>.description;
+
+ styles [
+ "subtitle-room",
+ "accent",
+ ]
+ }
+ };
};
+ }
+
+ content: $FlChannelMessages channel_messages {
+ manager: bind template.manager;
+ active-channel: bind channel_list.active-channel;
+ has-channels: bind channel_list.has-channels;
};
}
-
- content: $FlChannelMessages channel_messages {
- manager: bind template.manager;
- active-channel: bind channel_list.active-channel;
- has-channels: bind channel_list.has-channels;
- };
- }
- };
+ };
+ }
};
}
}
diff --git a/src/backend/dummy.rs b/src/backend/dummy.rs diff --git a/src/backend/dummy.rs b/src/backend/dummy.rs
index e6824e80..c1aae764 100644 index e6824e80..1ebff6f6 100644
--- a/src/backend/dummy.rs --- a/src/backend/dummy.rs
+++ b/src/backend/dummy.rs +++ b/src/backend/dummy.rs
@@ -382,18 +382,40 @@ impl super::Manager { @@ -382,6 +382,7 @@ impl super::Manager {
}, },
)); ));
- let msg_screenshot = msg!(self, "", 2, GROUP_ID, 19 + base_minute); +
- let screenshot_file = gtk::gio::File::for_uri("resource:///icon.svg"); let msg_screenshot = msg!(self, "", 2, GROUP_ID, 19 + base_minute);
- let attachment = crate::backend::Attachment::from_file(screenshot_file, self); let screenshot_file = gtk::gio::File::for_uri("resource:///icon.svg");
- msg_screenshot let attachment = crate::backend::Attachment::from_file(screenshot_file, self);
+ // A short question-and-answer in the 1-on-1 chat with contact 2 @@ -393,7 +394,7 @@ impl super::Manager {
+ // ("Developer") demonstrating the click-to-open image viewer
+ // (`gui::image_viewer`). The contact sends a photo of an apple
+ // (bundled into the screenshot-mode build via
+ // `data/resources/resources.gresource.xml.in` as `dummy_apple.jpg`,
+ // public domain "Big red apple.jpg" by Paolo Neo via Wikimedia
+ // Commons) along with a
+ // text question, and the local user answers with the obvious
+ // one-word reply.
+ let msg_fruit_question = msg!(
+ self,
+ "what is the name of this fruit?",
+ 2,
+ 2,
+ 30 + base_minute
+ );
+ // `Attachment::from_file` only loads images via filesystem paths; for
+ // `resource://` URIs `GFile::path()` returns None, so the image
+ // would never reach the Picture. Load the bytes directly out of the
+ // gresource bundle into a Texture instead, and build the attachment
+ // from that.
+ let fruit_pixbuf = gtk::gdk_pixbuf::Pixbuf::from_resource("/dummy_apple.jpg")
+ .expect("apple photo resource to load");
+ let fruit_texture = gtk::gdk::Texture::for_pixbuf(&fruit_pixbuf);
+ let fruit_photo = crate::backend::Attachment::from_texture(fruit_texture, self);
+ msg_fruit_question
.clone()
.downcast::<TextMessage>()
.unwrap()
- .add_attachment(attachment);
+ .add_attachment(fruit_photo);
+ let msg_fruit_answer = msg!(self, "apple", 0, 2, 32 + base_minute);
vec![ vec![
msg_replied, msg_replied,
- // msg_screenshot, - // msg_screenshot,
+ msg_screenshot,
msg_reply, msg_reply,
msg!( msg!(
self, self,
@@ -405,6 +427,8 @@ impl super::Manager {
msg!(self, "Glad you like it.", 2, GROUP_ID, 25 + base_minute),
msg!(self, "YAY!", 0, GROUP_ID, 27 + base_minute),
msg!(self, "Greetings", 2, 2, base_minute - 100),
+ msg_fruit_question,
+ msg_fruit_answer,
msg!(self, "It's a trap!", 3, 3, 1 + base_minute),
msg!(self, "Hello, Mr. Anderson", 4, 4, 2 + base_minute),
]
diff --git a/src/gui/components/attachment_image.rs b/src/gui/components/attachment_image.rs diff --git a/src/gui/components/attachment_image.rs b/src/gui/components/attachment_image.rs
index a91bf05b..4ddd29b6 100644 index a91bf05b..9ca6e0c7 100644
--- a/src/gui/components/attachment_image.rs --- a/src/gui/components/attachment_image.rs
+++ b/src/gui/components/attachment_image.rs +++ b/src/gui/components/attachment_image.rs
@@ -24,7 +24,10 @@ pub mod imp { @@ -24,7 +24,10 @@ pub mod imp {
@@ -527,7 +143,7 @@ index a91bf05b..4ddd29b6 100644
Utility::bind_template_callbacks(klass); Utility::bind_template_callbacks(klass);
} }
@@ -49,6 +53,33 @@ pub mod imp { @@ -49,6 +53,28 @@ pub mod imp {
} }
} }
@@ -543,11 +159,6 @@ index a91bf05b..4ddd29b6 100644
+ if attachment.image().is_none() { + if attachment.image().is_none() {
+ return; + return;
+ } + }
+ // Walk up to the top-level `crate::gui::Window` and ask it to
+ // host the viewer in its window-level overlay so the viewer
+ // covers the entire window content (sidebar + chat) rather
+ // than being constrained to the navigation split view's
+ // content pane like an Adw.Dialog would be.
+ let Some(window) = obj + let Some(window) = obj
+ .root() + .root()
+ .and_then(|r| r.dynamic_cast::<crate::gui::Window>().ok()) + .and_then(|r| r.dynamic_cast::<crate::gui::Window>().ok())
@@ -563,25 +174,15 @@ index a91bf05b..4ddd29b6 100644
self.parent_constructed(); self.parent_constructed();
diff --git a/src/gui/image_viewer.rs b/src/gui/image_viewer.rs diff --git a/src/gui/image_viewer.rs b/src/gui/image_viewer.rs
new file mode 100644 new file mode 100644
index 00000000..b3e9d720 index 00000000..a667a5a5
--- /dev/null --- /dev/null
+++ b/src/gui/image_viewer.rs +++ b/src/gui/image_viewer.rs
@@ -0,0 +1,120 @@ @@ -0,0 +1,57 @@
+use crate::prelude::*; +use crate::prelude::*;
+ +
+use crate::backend::Attachment; +use crate::backend::Attachment;
+ +
+glib::wrapper! { +glib::wrapper! {
+ /// A widget that displays an image attachment over a dark backdrop. It is
+ /// designed to be inserted into a top-level [`gtk::Overlay`] in
+ /// [`crate::gui::Window`] (see [`crate::gui::Window::present_image_viewer`])
+ /// so that it covers every other widget in the window — sidebar, chat
+ /// pane, header bar — rather than being constrained to the content area
+ /// of the surrounding navigation split view (which is what `Adw.Dialog`
+ /// would force).
+ ///
+ /// Emits `closed` when the user dismisses the viewer. Consumers should
+ /// remove the widget from the overlay in response.
+ pub struct ImageViewer(ObjectSubclass<imp::ImageViewer>) + pub struct ImageViewer(ObjectSubclass<imp::ImageViewer>)
+ @extends adw::Bin, gtk::Widget, + @extends adw::Bin, gtk::Widget,
+ @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget; + @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
@@ -589,7 +190,6 @@ index 00000000..b3e9d720
+ +
+impl ImageViewer { +impl ImageViewer {
+ pub fn new(attachment: &Attachment) -> Self { + pub fn new(attachment: &Attachment) -> Self {
+ log::trace!("Initializing `ImageViewer`");
+ Object::builder::<Self>() + Object::builder::<Self>()
+ .property("attachment", attachment) + .property("attachment", attachment)
+ .build() + .build()
@@ -599,8 +199,7 @@ index 00000000..b3e9d720
+pub mod imp { +pub mod imp {
+ use crate::prelude::*; + use crate::prelude::*;
+ +
+ use glib::subclass::{InitializingObject, Signal}; + use gtk::{subclass::prelude::*, CompositeTemplate, Picture};
+ use gtk::{CompositeTemplate, Picture};
+ +
+ use crate::backend::Attachment; + use crate::backend::Attachment;
+ +
@@ -615,41 +214,6 @@ index 00000000..b3e9d720
+ pub(super) picture: TemplateChild<Picture>, + pub(super) picture: TemplateChild<Picture>,
+ } + }
+ +
+ #[gtk::template_callbacks]
+ impl ImageViewer {
+ /// Click anywhere in the viewer (image or backdrop) closes. The
+ /// close button stops its own click before it reaches us via the
+ /// usual GTK click-bubbling, so the button still works.
+ #[template_callback]
+ fn on_backdrop_pressed(&self) {
+ self.obj().emit_by_name::<()>("closed", &[]);
+ }
+
+ #[template_callback]
+ fn on_close_clicked(&self) {
+ self.obj().emit_by_name::<()>("closed", &[]);
+ }
+
+ /// Close on `Escape`. The viewer grabs focus on construction so the
+ /// key controller actually sees keystrokes; without that, focus
+ /// would still be on whatever widget the user clicked on (typically
+ /// the message row) and `Escape` would route there instead.
+ #[template_callback]
+ fn on_key_pressed(
+ &self,
+ keyval: gdk::Key,
+ _keycode: u32,
+ _state: gdk::ModifierType,
+ ) -> glib::Propagation {
+ if keyval == gdk::Key::Escape {
+ self.obj().emit_by_name::<()>("closed", &[]);
+ glib::Propagation::Stop
+ } else {
+ glib::Propagation::Proceed
+ }
+ }
+ }
+
+ #[glib::object_subclass] + #[glib::object_subclass]
+ impl ObjectSubclass for ImageViewer { + impl ObjectSubclass for ImageViewer {
+ const NAME: &'static str = "FlImageViewer"; + const NAME: &'static str = "FlImageViewer";
@@ -658,7 +222,6 @@ index 00000000..b3e9d720
+ +
+ fn class_init(klass: &mut Self::Class) { + fn class_init(klass: &mut Self::Class) {
+ Self::bind_template(klass); + Self::bind_template(klass);
+ Self::bind_template_callbacks(klass);
+ } + }
+ +
+ fn instance_init(obj: &InitializingObject<Self>) { + fn instance_init(obj: &InitializingObject<Self>) {
@@ -667,22 +230,7 @@ index 00000000..b3e9d720
+ } + }
+ +
+ #[glib::derived_properties] + #[glib::derived_properties]
+ impl ObjectImpl for ImageViewer { + impl ObjectImpl for ImageViewer {}
+ fn signals() -> &'static [Signal] {
+ static SIGNALS: Lazy<Vec<Signal>> =
+ Lazy::new(|| vec![Signal::builder("closed").build()]);
+ SIGNALS.as_ref()
+ }
+
+ fn constructed(&self) {
+ self.parent_constructed();
+ // Make the widget focusable so the key controller below is
+ // actually allowed to receive keystrokes; Adw.Bin defaults to
+ // not-focusable.
+ self.obj().set_focusable(true);
+ self.obj().set_can_focus(true);
+ }
+ }
+ +
+ impl WidgetImpl for ImageViewer {} + impl WidgetImpl for ImageViewer {}
+ impl BinImpl for ImageViewer {} + impl BinImpl for ImageViewer {}
@@ -700,60 +248,23 @@ index 6b5cfd19..d2e997ee 100644
mod message_item; mod message_item;
mod new_channel_dialog; mod new_channel_dialog;
diff --git a/src/gui/window.rs b/src/gui/window.rs diff --git a/src/gui/window.rs b/src/gui/window.rs
index ce097ce1..bd9474e2 100644 index 6335f3d6..4f75e360 100644
--- a/src/gui/window.rs --- a/src/gui/window.rs
+++ b/src/gui/window.rs +++ b/src/gui/window.rs
@@ -104,6 +104,33 @@ impl Window { @@ -103,6 +103,13 @@ impl Window {
pub(crate) fn settings(&self) -> gio::Settings { pub(crate) fn settings(&self) -> gio::Settings {
self.imp().settings.clone() self.imp().settings.clone()
} }
+ +
+ /// Show an image viewer overlay covering the whole window content area
+ /// for `attachment`. Removes itself from the overlay when the viewer
+ /// emits `closed` (on `Escape`, click anywhere inside the viewer, or
+ /// the close button).
+ pub fn present_image_viewer(&self, attachment: &crate::backend::Attachment) { + pub fn present_image_viewer(&self, attachment: &crate::backend::Attachment) {
+ use crate::gui::image_viewer::ImageViewer; + let viewer = crate::gui::image_viewer::ImageViewer::new(attachment);
+ let viewer = ImageViewer::new(attachment); + let dialog = adw::Dialog::new();
+ let overlay = self.imp().image_viewer_overlay.clone(); + dialog.set_child(Some(&viewer));
+ overlay.add_overlay(&viewer); + dialog.present(Some(self.upcast_ref::<gtk::Widget>()));
+ viewer.grab_focus();
+ viewer.connect_local(
+ "closed",
+ false,
+ clone!(
+ #[weak]
+ overlay,
+ #[weak]
+ viewer,
+ #[upgrade_or_default]
+ move |_| {
+ overlay.remove_overlay(&viewer);
+ None
+ }
+ ),
+ );
+ } + }
} }
pub mod imp { pub mod imp {
@@ -142,6 +169,8 @@ pub mod imp {
#[template_child]
split_view: TemplateChild<adw::NavigationSplitView>,
#[template_child]
+ pub(super) image_viewer_overlay: TemplateChild<gtk::Overlay>,
+ #[template_child]
pub(super) channel_list: TemplateChild<ChannelList>,
#[template_child]
subtitle_label: TemplateChild<gtk::Label>,
@@ -165,6 +194,7 @@ pub mod imp {
channel_list: Default::default(),
subtitle_label: Default::default(),
channel_messages: Default::default(),
+ image_viewer_overlay: Default::default(),
new_channel_dialog: Default::default(),
manager: Default::default(),
settings: Settings::new(BASE_ID),
-- --
2.53.0 2.53.0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB