flare: update patches

This commit is contained in:
2026-05-07 17:32:58 -04:00
parent 7cf27f0cda
commit cfd62ea4ac
9 changed files with 777 additions and 8 deletions

View File

@@ -89,8 +89,8 @@
# 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, and the not-yet-merged init_channels # the deleted-message placeholder, the image-viewer overlay, and the
# 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;
cargoDeps = pkgs.rustPlatform.importCargoLock { cargoDeps = pkgs.rustPlatform.importCargoLock {
@@ -104,7 +104,17 @@
../../patches/flare/0004-feat-messages-In-channel-message-search.patch ../../patches/flare/0004-feat-messages-In-channel-message-search.patch
../../patches/flare/0005-feat-messages-Show-This-message-was-deleted.-placeho.patch ../../patches/flare/0005-feat-messages-Show-This-message-was-deleted.-placeho.patch
../../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
]; ];
# 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,7 +1,7 @@
From 7ab41203098dd868ee70249fcd78c8444438d80c Mon Sep 17 00:00:00 2001 From 7ab41203098dd868ee70249fcd78c8444438d80c Mon Sep 17 00:00:00 2001
From: Simon Gardling <titaniumtown@proton.me> From: Simon Gardling <titaniumtown@proton.me>
Date: Wed, 29 Apr 2026 19:00:12 -0400 Date: Wed, 29 Apr 2026 19:00:12 -0400
Subject: [PATCH 1/6] feat(typing): Implement typing indicators Subject: [PATCH 1/7] feat(typing): Implement typing indicators
- Send TypingMessage Started/Stopped events as the user composes a - Send TypingMessage Started/Stopped events as the user composes a
message, including a periodic refresh and an idle-stop timer so the message, including a periodic refresh and an idle-stop timer so the

View File

@@ -1,7 +1,7 @@
From f198aa5720bbc953fd72ebfffcec4f62f794fb19 Mon Sep 17 00:00:00 2001 From f198aa5720bbc953fd72ebfffcec4f62f794fb19 Mon Sep 17 00:00:00 2001
From: Simon Gardling <titaniumtown@proton.me> From: Simon Gardling <titaniumtown@proton.me>
Date: Wed, 29 Apr 2026 19:33:06 -0400 Date: Wed, 29 Apr 2026 19:33:06 -0400
Subject: [PATCH 2/6] feat(messages): Implement edited messages Subject: [PATCH 2/7] feat(messages): Implement edited messages
- Receive incoming EditMessage (1-1 and sync) and replace the body, - Receive incoming EditMessage (1-1 and sync) and replace the body,
body_ranges, and attachments of the targeted message in place. The body_ranges, and attachments of the targeted message in place. The

View File

@@ -1,7 +1,7 @@
From e6db982a5be5ca392538259dfbb66d38ca0d158d Mon Sep 17 00:00:00 2001 From e6db982a5be5ca392538259dfbb66d38ca0d158d Mon Sep 17 00:00:00 2001
From: Simon Gardling <titaniumtown@proton.me> From: Simon Gardling <titaniumtown@proton.me>
Date: Wed, 29 Apr 2026 19:53:22 -0400 Date: Wed, 29 Apr 2026 19:53:22 -0400
Subject: [PATCH 3/6] feat(messages): Multi-select messages and delete for me Subject: [PATCH 3/7] feat(messages): Multi-select messages and delete for me
- Add a 'Select' action to the message context menu that puts the - Add a 'Select' action to the message context menu that puts the
channel into selection mode and pre-selects the message. While in channel into selection mode and pre-selects the message. While in

View File

@@ -1,7 +1,7 @@
From 6272aee14e9ece866751b94b7ebd823f147f9531 Mon Sep 17 00:00:00 2001 From 6272aee14e9ece866751b94b7ebd823f147f9531 Mon Sep 17 00:00:00 2001
From: Simon Gardling <titaniumtown@proton.me> From: Simon Gardling <titaniumtown@proton.me>
Date: Wed, 29 Apr 2026 19:58:54 -0400 Date: Wed, 29 Apr 2026 19:58:54 -0400
Subject: [PATCH 4/6] feat(messages): In-channel message search Subject: [PATCH 4/7] feat(messages): In-channel message search
- Add a SearchBar above the message list that searches the - Add a SearchBar above the message list that searches the
currently-loaded timeline using a case-insensitive substring match currently-loaded timeline using a case-insensitive substring match

View File

@@ -1,7 +1,7 @@
From b8227bc71123c5ddb473a07884d33353e7078bf4 Mon Sep 17 00:00:00 2001 From b8227bc71123c5ddb473a07884d33353e7078bf4 Mon Sep 17 00:00:00 2001
From: Simon Gardling <titaniumtown@proton.me> From: Simon Gardling <titaniumtown@proton.me>
Date: Thu, 30 Apr 2026 04:25:07 -0400 Date: Thu, 30 Apr 2026 04:25:07 -0400
Subject: [PATCH 5/6] feat(messages): Show 'This message was deleted.' Subject: [PATCH 5/7] feat(messages): Show 'This message was deleted.'
placeholder placeholder
Upstream hides the whole MessageItem when is-deleted is true via a Upstream hides the whole MessageItem when is-deleted is true via a

View File

@@ -1,7 +1,7 @@
From 1546f95d8b6666a7f0e9575ddfa85bcb7ec53e3d Mon Sep 17 00:00:00 2001 From 1546f95d8b6666a7f0e9575ddfa85bcb7ec53e3d Mon Sep 17 00:00:00 2001
From: Simon Gardling <titaniumtown@proton.me> From: Simon Gardling <titaniumtown@proton.me>
Date: Wed, 6 May 2026 14:38:07 -0400 Date: Wed, 6 May 2026 14:38:07 -0400
Subject: [PATCH 6/6] fix(backend): Refresh cached channels on init_channels Subject: [PATCH 6/7] fix(backend): Refresh cached channels on init_channels
re-runs re-runs
init_channels populates the channel cache via or_insert_with, then init_channels populates the channel cache via or_insert_with, then

View File

@@ -0,0 +1,759 @@
From e13d8cbf15a2eea323397accb2183e2362dae106 Mon Sep 17 00:00:00 2001
From: Simon Gardling <titaniumtown@proton.me>
Date: Wed, 6 May 2026 18:41:15 -0400
Subject: [PATCH 7/7] feat(messages): Open image attachments in a
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
"pressed" gesture is right-click-only and routes to the context menu.
To actually look at an image at full resolution the user has to
right-click and choose "Open", which dispatches the file through the
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 +
data/resources/meson.build | 1 +
data/resources/resources.gresource.xml.in | 5 +
data/resources/style.css | 8 +-
.../ui/components/attachment_image.blp | 5 +
data/resources/ui/image_viewer.blp | 64 +++++
data/resources/ui/window.blp | 224 +++++++++---------
src/backend/dummy.rs | 36 ++-
src/gui/components/attachment_image.rs | 33 ++-
src/gui/image_viewer.rs | 120 ++++++++++
src/gui/mod.rs | 1 +
src/gui/window.rs | 30 +++
13 files changed, 411 insertions(+), 117 deletions(-)
create mode 100644 data/resources/ui/image_viewer.blp
create mode 100644 src/gui/image_viewer.rs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e221f555..8ebca71c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ 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.
## [0.20.4] - 2026-04-22
diff --git a/data/resources/meson.build b/data/resources/meson.build
index 7fc00b7c..efd789b7 100644
--- a/data/resources/meson.build
+++ b/data/resources/meson.build
@@ -11,6 +11,7 @@ blueprints = custom_target('blueprints',
'ui/dialog_clear_messages.blp',
'ui/dialog_unlink.blp',
'ui/error_dialog.blp',
+ 'ui/image_viewer.blp',
'ui/setup_window.blp',
'ui/linked_devices_window.blp',
'ui/device_info_item.blp',
diff --git a/data/resources/resources.gresource.xml.in b/data/resources/resources.gresource.xml.in
index 8604fe24..863564b3 100644
--- a/data/resources/resources.gresource.xml.in
+++ b/data/resources/resources.gresource.xml.in
@@ -2,6 +2,10 @@
<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/device_info_item.ui</file>
<file preprocess="xml-stripblanks">ui/error_dialog.ui</file>
+ <file preprocess="xml-stripblanks">ui/image_viewer.ui</file>
<file preprocess="xml-stripblanks">ui/attachment.ui</file>
<file preprocess="xml-stripblanks">ui/preferences_window.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
index ec303a72..bcbdabe1 100644
--- a/data/resources/ui/components/attachment_image.blp
+++ b/data/resources/ui/components/attachment_image.blp
@@ -2,6 +2,11 @@ using Gtk 4.0;
template $FlAttachmentImage: $FlAttachmentBase {
Picture picture {
+ GestureClick {
+ button: 1;
+ pressed => $on_clicked() swapped;
+ }
+
styles [
"photo",
]
diff --git a/data/resources/ui/image_viewer.blp b/data/resources/ui/image_viewer.blp
new file mode 100644
index 00000000..1b27cc09
--- /dev/null
+++ b/data/resources/ui/image_viewer.blp
@@ -0,0 +1,64 @@
+using Gtk 4.0;
+using Adw 1;
+
+template $FlImageViewer: Adw.Bin {
+ hexpand: 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 {
+ paintable: bind template.attachment as <$FlAttachmentObject>.image;
+ content-fit: contain;
+ can-shrink: true;
+ hexpand: 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
index e6824e80..c1aae764 100644
--- a/src/backend/dummy.rs
+++ b/src/backend/dummy.rs
@@ -382,18 +382,40 @@ 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 attachment = crate::backend::Attachment::from_file(screenshot_file, self);
- msg_screenshot
+ // A short question-and-answer in the 1-on-1 chat with contact 2
+ // ("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![
msg_replied,
- // msg_screenshot,
msg_reply,
msg!(
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
index a91bf05b..4ddd29b6 100644
--- a/src/gui/components/attachment_image.rs
+++ b/src/gui/components/attachment_image.rs
@@ -24,7 +24,10 @@ pub mod imp {
use glib::subclass::InitializingObject;
use gtk::{CompositeTemplate, Picture};
- use crate::gui::{attachment::Attachment, attachment::AttachmentImpl, utility::Utility};
+ use crate::gui::{
+ attachment::{Attachment, AttachmentImpl},
+ utility::Utility,
+ };
#[derive(CompositeTemplate, Default)]
#[template(resource = "/ui/components/attachment_image.ui")]
@@ -41,6 +44,7 @@ pub mod imp {
fn class_init(klass: &mut Self::Class) {
Self::bind_template(klass);
+ Self::bind_template_callbacks(klass);
Utility::bind_template_callbacks(klass);
}
@@ -49,6 +53,33 @@ pub mod imp {
}
}
+ #[gtk::template_callbacks]
+ impl AttachmentImage {
+ /// Open the image in a viewer presented inside the parent window.
+ /// Bails out silently if the attachment's texture hasn't loaded yet.
+ #[template_callback]
+ fn on_clicked(&self) {
+ let obj = self.obj();
+ let base = obj.upcast_ref::<Attachment>();
+ let attachment = base.attachment();
+ if attachment.image().is_none() {
+ 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
+ .root()
+ .and_then(|r| r.dynamic_cast::<crate::gui::Window>().ok())
+ else {
+ return;
+ };
+ window.present_image_viewer(&attachment);
+ }
+ }
+
impl ObjectImpl for AttachmentImage {
fn constructed(&self) {
self.parent_constructed();
diff --git a/src/gui/image_viewer.rs b/src/gui/image_viewer.rs
new file mode 100644
index 00000000..b3e9d720
--- /dev/null
+++ b/src/gui/image_viewer.rs
@@ -0,0 +1,120 @@
+use crate::prelude::*;
+
+use crate::backend::Attachment;
+
+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>)
+ @extends adw::Bin, gtk::Widget,
+ @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget;
+}
+
+impl ImageViewer {
+ pub fn new(attachment: &Attachment) -> Self {
+ log::trace!("Initializing `ImageViewer`");
+ Object::builder::<Self>()
+ .property("attachment", attachment)
+ .build()
+ }
+}
+
+pub mod imp {
+ use crate::prelude::*;
+
+ use glib::subclass::{InitializingObject, Signal};
+ use gtk::{CompositeTemplate, Picture};
+
+ use crate::backend::Attachment;
+
+ #[derive(CompositeTemplate, Default, glib::Properties)]
+ #[properties(wrapper_type = super::ImageViewer)]
+ #[template(resource = "/ui/image_viewer.ui")]
+ pub struct ImageViewer {
+ #[property(get, set, construct_only)]
+ attachment: RefCell<Option<Attachment>>,
+
+ #[template_child]
+ 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]
+ impl ObjectSubclass for ImageViewer {
+ const NAME: &'static str = "FlImageViewer";
+ type Type = super::ImageViewer;
+ type ParentType = adw::Bin;
+
+ fn class_init(klass: &mut Self::Class) {
+ Self::bind_template(klass);
+ Self::bind_template_callbacks(klass);
+ }
+
+ fn instance_init(obj: &InitializingObject<Self>) {
+ obj.init_template();
+ }
+ }
+
+ #[glib::derived_properties]
+ 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 BinImpl for ImageViewer {}
+}
diff --git a/src/gui/mod.rs b/src/gui/mod.rs
index 6b5cfd19..d2e997ee 100644
--- a/src/gui/mod.rs
+++ b/src/gui/mod.rs
@@ -8,6 +8,7 @@ mod channel_messages;
mod components;
mod device_info_item;
mod error_dialog;
+mod image_viewer;
mod linked_devices_window;
mod message_item;
mod new_channel_dialog;
diff --git a/src/gui/window.rs b/src/gui/window.rs
index ce097ce1..bd9474e2 100644
--- a/src/gui/window.rs
+++ b/src/gui/window.rs
@@ -104,6 +104,33 @@ impl Window {
pub(crate) fn settings(&self) -> gio::Settings {
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) {
+ use crate::gui::image_viewer::ImageViewer;
+ let viewer = ImageViewer::new(attachment);
+ let overlay = self.imp().image_viewer_overlay.clone();
+ overlay.add_overlay(&viewer);
+ 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 {
@@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB