Repository: GyulyVGC/sniffnet (language: rust)

Issue:
[Bug] Cannot exit Thumbnail mode on Linux
### Is there an existing issue for this?

- [X] I have searched the existing issues.

### What's the problem?

Minimize works but maximize back isn't working.

See vidéo below:

https://github.com/GyulyVGC/sniffnet/assets/13169819/d6cbb6c3-fe04-425b-8fc1-a49a174776be



### How did you install the app?

downloaded one of the provided packages

### Operating System

Linux

### Additional context

_No response_


Discussion / hints:
Just adding a comment Off-Topic to give you a lot of kudos for the UI refactoring!!! You've done an amazing work. 

PS: note that this issue is non-blocking, so take your time to fix it.
Hey @matbgn thanks so much for the kudos, I really appreciate you like it 😄 

***

### The problem

This annoying issue seems to happen only in some Linux systems, and it's due to the fact that when thumbnail mode is active click events on the window may indicate that the user started grabbing it.

While Windows and macOS correctly distinguish when the user clicked on the "maximise" button, in Linux this action is interpreted as a click to start dragging the thumbnail (some environments require a double click to work, while in other cases as yours it's not possible even with multiple clicks).

***

### Possible solutions

- A possible workaround I can think of is to add a keyboard shortcut to enter/exit thumbnail mode, so users with this issue can simply focus the app window and press the shortcut key to maximise it back
- Since the previous point is not a real solution to the main problem, the best strategy would be to set the drag hook for the window excluding the portion where the "maximise" button is... but I'm currently not aware about how to tell Iced to listen to drag events only for a given portion of the window
For what it's worth, it works in Gnome Wayland, but only if I double click on the thumbnail icon. 

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index 265a637e9..a628928a5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,9 +25,6 @@ include = [
     "/build.rs",
 ]
 
-[target."cfg(windows)"]
-rustflags = ["-C", "target-feature=+crt-static"]
-
 #═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════
 
 [profile.release]
diff --git a/src/gui/app.rs b/src/gui/app.rs
index 2c2c2d08b..7ce98eddd 100644
--- a/src/gui/app.rs
+++ b/src/gui/app.rs
@@ -2,15 +2,8 @@
 //!
 //! It also is a wrapper of gui's main two pages: initial and run page.
 
-use std::time::Duration;
-
-use iced::keyboard::key::Named;
-use iced::keyboard::{Event, Key, Modifiers};
-use iced::mouse::Event::ButtonPressed;
 use iced::widget::Column;
-use iced::window::Id;
-use iced::Event::{Keyboard, Window};
-use iced::{executor, window, Application, Command, Element, Subscription};
+use iced::{executor, Application, Command, Element, Subscription};
 
 use crate::gui::components::footer::footer;
 use crate::gui::components::header::header;
@@ -122,66 +115,12 @@ impl Application for Sniffer {
     }
 
     fn subscription(&self) -> Subscription<Message> {
-        const NO_MODIFIER: Modifiers = Modifiers::empty();
-
-        // Window subscription
-        let window_sub = iced::event::listen_with(|event, _| match event {
-            Window(Id::MAIN, window::Event::Focused) => Some(Message::WindowFocused),
-            Window(Id::MAIN, window::Event::Moved { x, y }) => Some(Message::WindowMoved(x, y)),
-            Window(Id::MAIN, window::Event::Resized { width, height }) => {
-                Some(Message::WindowResized(width, height))
-            }
-            Window(Id::MAIN, window::Event::CloseRequested) => Some(Message::CloseRequested),
-            _ => None,
-        });
-
-        // Keyboard subscription
-        let keyboard_sub = iced::event::listen_with(|event, _| match event {
-            Keyboard(Event::KeyPressed { key, modifiers, .. }) => match modifiers {
-                Modifiers::COMMAND => match key.as_ref() {
-                    Key::Character("q") => Some(Message::CloseRequested),
-                    Key::Character(",") => Some(Message::OpenLastSettings),
-                    Key::Named(Named::Backspace) => Some(Message::ResetButtonPressed),
-                    Key::Character("d") => Some(Message::CtrlDPressed),
-                    Key::Named(Named::ArrowLeft) => Some(Message::ArrowPressed(false)),
-                    Key::Named(Named::ArrowRight) => Some(Message::ArrowPressed(true)),
-                    _ => None,
-                },
-                Modifiers::SHIFT => match key {
-                    Key::Named(Named::Tab) => Some(Message::SwitchPage(false)),
-                    _ => None,
-                },
-                NO_MODIFIER => match key {
-                    Key::Named(Named::Enter) => Some(Message::ReturnKeyPressed),
-                    Key::Named(Named::Escape) => Some(Message::EscKeyPressed),
-                    Key::Named(Named::Tab) => Some(Message::SwitchPage(true)),
-                    _ => None,
-                },
-                _ => None,
-            },
-            _ => None,
-        });
-
-        // Mouse subscription
-        let mouse_sub = iced::event::listen_with(|event, _| match event {
-            iced::event::Event::Mouse(ButtonPressed(_)) => Some(Message::Drag),
-            _ => None,
-        });
-
-        // Time subscription
-        let time_sub = if self.running_page.eq(&RunningPage::Init) {
-            iced::time::every(Duration::from_millis(PERIOD_TICK)).map(|_| Message::TickInit)
-        } else {
-            iced::time::every(Duration::from_millis(PERIOD_TICK)).map(|_| Message::TickRun)
-        };
-
-        let mut subscriptions = Vec::from([window_sub, time_sub]);
-        if self.thumbnail {
-            subscriptions.push(mouse_sub);
-        } else {
-            subscriptions.push(keyboard_sub);
-        }
-        Subscription::batch(subscriptions)
+        Subscription::batch([
+            self.keyboard_subscription(),
+            self.mouse_subscription(),
+            self.time_subscription(),
+            Sniffer::window_subscription(),
+        ])
     }
 
     fn theme(&self) -> Self::Theme {
diff --git a/src/gui/pages/thumbnail_page.rs b/src/gui/pages/thumbnail_page.rs
index 2d0f69166..6a5ab420e 100644
--- a/src/gui/pages/thumbnail_page.rs
+++ b/src/gui/pages/thumbnail_page.rs
@@ -13,7 +13,7 @@ use crate::gui::styles::style_constants::FONT_SIZE_FOOTER;
 use crate::gui::styles::types::style_type::StyleType;
 use crate::gui::types::message::Message;
 use crate::gui::types::sniffer::Sniffer;
-use crate::networking::types::host::Host;
+use crate::networking::types::host::{Host, ThumbnailHost};
 use crate::networking::types::info_traffic::InfoTraffic;
 use crate::report::get_report_entries::{get_host_entries, get_service_entries};
 use crate::report::types::sort_type::SortType;
@@ -71,22 +71,35 @@ fn host_col(
         .spacing(3)
         .width(Length::FillPortion(2));
     let hosts = get_host_entries(info_traffic, chart_type, SortType::Neutral);
-    let n_entry = min(hosts.len(), MAX_ENTRIES);
-    for (host, data_info_host) in hosts.get(..n_entry).unwrap_or_default() {
-        let flag = get_flag_tooltip(
-            host.country,
-            data_info_host,
-            Language::default(),
-            font,
-            true,
-        );
+    let mut thumbnail_hosts = Vec::new();
+
+    for (host, data_info_host) in &hosts {
+        let text = host_text(host);
+        let country = host.country;
+        let thumbnail_host = ThumbnailHost {
+            country,
+            text: text.clone(),
+        };
+
+        if thumbnail_hosts.contains(&thumbnail_host) {
+            continue;
+        }
+
+        thumbnail_hosts.push(thumbnail_host);
+
+        let flag = get_flag_tooltip(country, data_info_host, Language::default(), font, true);
         let host_row = Row::new()
             .align_items(Alignment::Center)
             .spacing(5)
             .push(flag)
-            .push(Text::new(host_text(host)).font(font).size(FONT_SIZE_FOOTER));
+            .push(Text::new(text).font(font).size(FONT_SIZE_FOOTER));
         host_col = host_col.push(host_row);
+
+        if thumbnail_hosts.len() >= MAX_ENTRIES {
+            break;
+        }
     }
+
     host_col
 }
 
diff --git a/src/gui/types/message.rs b/src/gui/types/message.rs
index a94b4f991..d5d0b8ad6 100644
--- a/src/gui/types/message.rs
+++ b/src/gui/types/message.rs
@@ -115,4 +115,6 @@ pub enum Message {
     ToggleThumbnail(bool),
     /// Drag the window
     Drag,
+    /// Ctrl+T keys have been pressed
+    CtrlTPressed,
 }
diff --git a/src/gui/types/sniffer.rs b/src/gui/types/sniffer.rs
index 70b57286e..7e5b64418 100644
--- a/src/gui/types/sniffer.rs
+++ b/src/gui/types/sniffer.rs
@@ -5,14 +5,20 @@ use std::collections::{HashSet, VecDeque};
 use std::path::PathBuf;
 use std::sync::{Arc, Mutex};
 use std::thread;
+use std::time::Duration;
 
+use iced::keyboard::key::Named;
+use iced::keyboard::{Event, Key, Modifiers};
+use iced::mouse::Event::ButtonPressed;
 use iced::window::{Id, Level};
-use iced::{window, Command};
+use iced::Event::{Keyboard, Window};
+use iced::{window, Command, Subscription};
 use pcap::Device;
 use rfd::FileHandle;
 
 use crate::chart::manage_chart_data::update_charts_data;
 use crate::configs::types::config_window::{ConfigWindow, ScaleAndCheck, ToPoint, ToSize};
+use crate::gui::app::PERIOD_TICK;
 use crate::gui::components::types::my_modal::MyModal;
 use crate::gui::pages::types::running_page::RunningPage;
 use crate::gui::pages::types::settings_page::SettingsPage;
@@ -142,6 +148,83 @@ impl Sniffer {
         }
     }
 
+    pub(crate) fn keyboard_subscription(&self) -> Subscription<Message> {
+        const NO_MODIFIER: Modifiers = Modifiers::empty();
+
+        if self.thumbnail {
+            iced::event::listen_with(|event, _| match event {
+                Keyboard(Event::KeyPressed {
+                    key,
+                    modifiers: Modifiers::COMMAND,
+                    ..
+                }) => match key.as_ref() {
+                    Key::Character("q") => Some(Message::CloseRequested),
+                    Key::Character("t") => Some(Message::CtrlTPressed),
+                    _ => None,
+                },
+                _ => None,
+            })
+        } else {
+            iced::event::listen_with(|event, _| match event {
+                Keyboard(Event::KeyPressed { key, modifiers, .. }) => match modifiers {
+                    Modifiers::COMMAND => match key.as_ref() {
+                        Key::Character("q") => Some(Message::CloseRequested),
+                        Key::Character("t") => Some(Message::CtrlTPressed),
+                        Key::Character(",") => Some(Message::OpenLastSettings),
+                        Key::Named(Named::Backspace) => Some(Message::ResetButtonPressed),
+                        Key::Character("d") => Some(Message::CtrlDPressed),
+                        Key::Named(Named::ArrowLeft) => Some(Message::ArrowPressed(false)),
+                        Key::Named(Named::ArrowRight) => Some(Message::ArrowPressed(true)),
+                        _ => None,
+                    },
+                    Modifiers::SHIFT => match key {
+                        Key::Named(Named::Tab) => Some(Message::SwitchPage(false)),
+                        _ => None,
+                    },
+                    NO_MODIFIER => match key {
+                        Key::Named(Named::Enter) => Some(Message::ReturnKeyPressed),
+                        Key::Named(Named::Escape) => Some(Message::EscKeyPressed),
+                        Key::Named(Named::Tab) => Some(Message::SwitchPage(true)),
+                        _ => None,
+                    },
+                    _ => None,
+                },
+                _ => None,
+            })
+        }
+    }
+
+    pub(crate) fn mouse_subscription(&self) -> Subscription<Message> {
+        if self.thumbnail {
+            iced::event::listen_with(|event, _| match event {
+                iced::event::Event::Mouse(ButtonPressed(_)) => Some(Message::Drag),
+                _ => None,
+            })
+        } else {
+            Subscription::none()
+        }
+    }
+
+    pub(crate) fn time_subscription(&self) -> Subscription<Message> {
+        if self.running_page.eq(&RunningPage::Init) {
+            iced::time::every(Duration::from_millis(PERIOD_TICK)).map(|_| Message::TickInit)
+        } else {
+            iced::time::every(Duration::from_millis(PERIOD_TICK)).map(|_| Message::TickRun)
+        }
+    }
+
+    pub(crate) fn window_subscription() -> Subscription<Message> {
+        iced::event::listen_with(|event, _| match event {
+            Window(Id::MAIN, window::Event::Focused) => Some(Message::WindowFocused),
+            Window(Id::MAIN, window::Event::Moved { x, y }) => Some(Message::WindowMoved(x, y)),
+            Window(Id::MAIN, window::Event::Resized { width, height }) => {
+                Some(Message::WindowResized(width, height))
+            }
+            Window(Id::MAIN, window::Event::CloseRequested) => Some(Message::CloseRequested),
+            _ => None,
+        })
+    }
+
     pub fn update(&mut self, message: Message) -> Command<Message> {
         match message {
             Message::TickRun => return self.refresh_data(),
@@ -390,7 +473,20 @@ impl Sniffer {
                 };
             }
             Message::Drag => {
-                return window::drag(Id::MAIN);
+                let was_just_thumbnail_click = self.timing_events.was_just_thumbnail_click();
+                self.timing_events.thumbnail_click_now();
+                if was_just_thumbnail_click {
+                    return window::drag(Id::MAIN);
+                }
+            }
+            Message::CtrlTPressed => {
+                if self.running_page.ne(&RunningPage::Init)
+                    && self.settings_page.is_none()
+                    && self.modal.is_none()
+                    && !self.timing_events.was_just_thumbnail_enter()
+                {
+                    return self.update(Message::ToggleThumbnail(false));
+                }
             }
             Message::TickInit => {}
         }
@@ -703,10 +799,8 @@ mod tests {
 
     use std::collections::{HashSet, VecDeque};
     use std::fs::remove_file;
-    use std::ops::Sub;
     use std::path::Path;
     use std::sync::{Arc, Mutex};
-    use std::time::Duration;
 
     use serial_test::{parallel, serial};
 
@@ -1600,7 +1694,6 @@ mod tests {
     #[parallel] // needed to not collide with other tests generating configs files
     fn test_correctly_switch_running_and_settings_pages() {
         let mut sniffer = new_sniffer();
-        sniffer.timing_events.focus = std::time::Instant::now().sub(Duration::from_millis(400));
 
         // initial status
         assert_eq!(sniffer.settings_page, None);
diff --git a/src/gui/types/timing_events.rs b/src/gui/types/timing_events.rs
index 74320c995..4f7eca635 100644
--- a/src/gui/types/timing_events.rs
+++ b/src/gui/types/timing_events.rs
@@ -1,18 +1,22 @@
+use std::ops::Sub;
 use std::time::Duration;
 
 pub struct TimingEvents {
     /// Instant of the last window focus
-    pub(crate) focus: std::time::Instant,
+    focus: std::time::Instant,
     /// Instant of the last press on Copy IP button, with the related IP address
     copy_ip: (std::time::Instant, String),
     /// Instant of the last thumbnail mode enter
     thumbnail_enter: std::time::Instant,
+    /// Instant of the last click on the thumbnail window
+    thumbnail_click: std::time::Instant,
 }
 
 impl TimingEvents {
     const TIMEOUT_FOCUS: u64 = 200;
     const TIMEOUT_COPY_IP: u64 = 1500;
     const TIMEOUT_THUMBNAIL_ENTER: u64 = 1000;
+    const TIMEOUT_THUMBNAIL_CLICK: u64 = 800;
 
     pub fn focus_now(&mut self) {
         self.focus = std::time::Instant::now();
@@ -39,14 +43,24 @@ impl TimingEvents {
         self.thumbnail_enter.elapsed()
             < Duration::from_millis(TimingEvents::TIMEOUT_THUMBNAIL_ENTER)
     }
+
+    pub fn thumbnail_click_now(&mut self) {
+        self.thumbnail_click = std::time::Instant::now();
+    }
+
+    pub fn was_just_thumbnail_click(&self) -> bool {
+        self.thumbnail_click.elapsed()
+            < Duration::from_millis(TimingEvents::TIMEOUT_THUMBNAIL_CLICK)
+    }
 }
 
 impl Default for TimingEvents {
     fn default() -> Self {
         Self {
-            focus: std::time::Instant::now(),
+            focus: std::time::Instant::now().sub(Duration::from_millis(400)),
             copy_ip: (std::time::Instant::now(), String::new()),
             thumbnail_enter: std::time::Instant::now(),
+            thumbnail_click: std::time::Instant::now(),
         }
     }
 }
diff --git a/src/networking/types/host.rs b/src/networking/types/host.rs
index 6e9ba27da..7d06eaad4 100644
--- a/src/networking/types/host.rs
+++ b/src/networking/types/host.rs
@@ -11,3 +11,15 @@ pub struct Host {
     /// Country
     pub country: Country,
 }
+
+/// Struct to represent a network host for representation in the thumbnail
+///
+/// This is necessary to remove possible duplicates in the thumbnail host list
+#[allow(clippy::module_name_repetitions)]
+#[derive(PartialEq)]
+pub struct ThumbnailHost {
+    /// Country
+    pub country: Country,
+    /// Text describing the host in the thumbnail
+    pub text: String,
+}


Test patch: