flare: update image patch
This commit is contained in:
@@ -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>
|
||||
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
|
||||
Date: Tue, 12 May 2026 12:03:33 -0400
|
||||
Subject: [PATCH] feat(messages): Open image attachments in a dialog
|
||||
|
||||
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`.
|
||||
Mirrors the official Signal desktop application by allowing the user
|
||||
to click on an image attachment and see it expanded in an Adw.Dialog
|
||||
overlay with built-in close button, Escape handling, and mobile
|
||||
swipe-down support.
|
||||
---
|
||||
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(-)
|
||||
CHANGELOG.md | 4 ++
|
||||
data/resources/meson.build | 1 +
|
||||
data/resources/resources.gresource.xml.in | 1 +
|
||||
.../ui/components/attachment_image.blp | 5 ++
|
||||
data/resources/ui/image_viewer.blp | 15 +++++
|
||||
src/backend/dummy.rs | 3 +-
|
||||
src/gui/components/attachment_image.rs | 28 ++++++++-
|
||||
src/gui/image_viewer.rs | 57 +++++++++++++++++++
|
||||
src/gui/mod.rs | 1 +
|
||||
src/gui/window.rs | 7 +++
|
||||
10 files changed, 120 insertions(+), 2 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
|
||||
index 20dc578e..1e4649bc 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.
|
||||
@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [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
|
||||
|
||||
### Fixed
|
||||
diff --git a/data/resources/meson.build b/data/resources/meson.build
|
||||
index 7fc00b7c..efd789b7 100644
|
||||
--- a/data/resources/meson.build
|
||||
@@ -68,21 +50,10 @@ index 7fc00b7c..efd789b7 100644
|
||||
'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
|
||||
index 8604fe24..7c958d02 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 @@
|
||||
@@ -13,6 +13,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>
|
||||
@@ -90,23 +61,6 @@ index 8604fe24..863564b3 100644
|
||||
<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
|
||||
@@ -125,386 +79,48 @@ index ec303a72..bcbdabe1 100644
|
||||
]
|
||||
diff --git a/data/resources/ui/image_viewer.blp b/data/resources/ui/image_viewer.blp
|
||||
new file mode 100644
|
||||
index 00000000..1b27cc09
|
||||
index 00000000..19660f0b
|
||||
--- /dev/null
|
||||
+++ b/data/resources/ui/image_viewer.blp
|
||||
@@ -0,0 +1,64 @@
|
||||
@@ -0,0 +1,15 @@
|
||||
+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 {
|
||||
+ Picture picture {
|
||||
+ paintable: bind template.attachment as <$FlAttachmentObject>.image;
|
||||
+ content-fit: contain;
|
||||
+ can-shrink: true;
|
||||
+ 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
|
||||
index e6824e80..1ebff6f6 100644
|
||||
--- a/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 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);
|
||||
+
|
||||
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);
|
||||
@@ -393,7 +394,7 @@ impl super::Manager {
|
||||
|
||||
vec![
|
||||
msg_replied,
|
||||
- // msg_screenshot,
|
||||
+ 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
|
||||
index a91bf05b..9ca6e0c7 100644
|
||||
--- a/src/gui/components/attachment_image.rs
|
||||
+++ b/src/gui/components/attachment_image.rs
|
||||
@@ -24,7 +24,10 @@ pub mod imp {
|
||||
@@ -527,7 +143,7 @@ index a91bf05b..4ddd29b6 100644
|
||||
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() {
|
||||
+ 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())
|
||||
@@ -563,25 +174,15 @@ index a91bf05b..4ddd29b6 100644
|
||||
self.parent_constructed();
|
||||
diff --git a/src/gui/image_viewer.rs b/src/gui/image_viewer.rs
|
||||
new file mode 100644
|
||||
index 00000000..b3e9d720
|
||||
index 00000000..a667a5a5
|
||||
--- /dev/null
|
||||
+++ b/src/gui/image_viewer.rs
|
||||
@@ -0,0 +1,120 @@
|
||||
@@ -0,0 +1,57 @@
|
||||
+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;
|
||||
@@ -589,7 +190,6 @@ index 00000000..b3e9d720
|
||||
+
|
||||
+impl ImageViewer {
|
||||
+ pub fn new(attachment: &Attachment) -> Self {
|
||||
+ log::trace!("Initializing `ImageViewer`");
|
||||
+ Object::builder::<Self>()
|
||||
+ .property("attachment", attachment)
|
||||
+ .build()
|
||||
@@ -599,8 +199,7 @@ index 00000000..b3e9d720
|
||||
+pub mod imp {
|
||||
+ use crate::prelude::*;
|
||||
+
|
||||
+ use glib::subclass::{InitializingObject, Signal};
|
||||
+ use gtk::{CompositeTemplate, Picture};
|
||||
+ use gtk::{subclass::prelude::*, CompositeTemplate, Picture};
|
||||
+
|
||||
+ use crate::backend::Attachment;
|
||||
+
|
||||
@@ -615,41 +214,6 @@ index 00000000..b3e9d720
|
||||
+ 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";
|
||||
@@ -658,7 +222,6 @@ index 00000000..b3e9d720
|
||||
+
|
||||
+ fn class_init(klass: &mut Self::Class) {
|
||||
+ Self::bind_template(klass);
|
||||
+ Self::bind_template_callbacks(klass);
|
||||
+ }
|
||||
+
|
||||
+ fn instance_init(obj: &InitializingObject<Self>) {
|
||||
@@ -667,22 +230,7 @@ index 00000000..b3e9d720
|
||||
+ }
|
||||
+
|
||||
+ #[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 ObjectImpl for ImageViewer {}
|
||||
+
|
||||
+ impl WidgetImpl for ImageViewer {}
|
||||
+ impl BinImpl for ImageViewer {}
|
||||
@@ -700,60 +248,23 @@ index 6b5cfd19..d2e997ee 100644
|
||||
mod message_item;
|
||||
mod new_channel_dialog;
|
||||
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
|
||||
+++ b/src/gui/window.rs
|
||||
@@ -104,6 +104,33 @@ impl Window {
|
||||
@@ -103,6 +103,13 @@ 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
|
||||
+ }
|
||||
+ ),
|
||||
+ );
|
||||
+ let viewer = crate::gui::image_viewer::ImageViewer::new(attachment);
|
||||
+ let dialog = adw::Dialog::new();
|
||||
+ dialog.set_child(Some(&viewer));
|
||||
+ dialog.present(Some(self.upcast_ref::<gtk::Widget>()));
|
||||
+ }
|
||||
}
|
||||
|
||||
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.
|
Before Width: | Height: | Size: 276 KiB |
Reference in New Issue
Block a user