diff --git a/CHANGELOG.md b/CHANGELOG.md index 54d8b5507..72e8ca0fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All Sniffnet releases with the relative changes are documented in this file. ## [UNRELEASED] +- Donut chart reporting overall traffic statistics ([#756](https://github.com/GyulyVGC/sniffnet/pull/756) — fixes [#687](https://github.com/GyulyVGC/sniffnet/issues/687)) - Added support for ARP protocol ([#759](https://github.com/GyulyVGC/sniffnet/pull/759) — fixes [#680](https://github.com/GyulyVGC/sniffnet/issues/680)) - Identify and tag unassigned/reserved "bogon" IP addresses ([#678](https://github.com/GyulyVGC/sniffnet/pull/678) — fixes [#209](https://github.com/GyulyVGC/sniffnet/issues/209)) - Show data agglomerates in _Inspect_ page table ([#684](https://github.com/GyulyVGC/sniffnet/pull/684) — fixes [#601](https://github.com/GyulyVGC/sniffnet/issues/601)) diff --git a/src/chart/types/donut_chart.rs b/src/chart/types/donut_chart.rs new file mode 100644 index 000000000..542428892 --- /dev/null +++ b/src/chart/types/donut_chart.rs @@ -0,0 +1,162 @@ +use crate::chart::types::chart_type::ChartType; +use crate::gui::styles::donut::Catalog; +use crate::gui::styles::style_constants::FONT_SIZE_SUBTITLE; +use crate::networking::types::byte_multiple::ByteMultiple; +use iced::alignment::{Horizontal, Vertical}; +use iced::widget::canvas::path::Arc; +use iced::widget::canvas::{Frame, Text}; +use iced::widget::{canvas, Canvas}; +use iced::{mouse, Font, Radians, Renderer}; +use std::f32::consts; + +pub struct DonutChart { + chart_type: ChartType, + incoming: u128, + outgoing: u128, + filtered_out: u128, + dropped: u128, + font: Font, +} + +impl DonutChart { + fn new( + chart_type: ChartType, + incoming: u128, + outgoing: u128, + filtered_out: u128, + dropped: u128, + font: Font, + ) -> Self { + Self { + chart_type, + incoming, + outgoing, + filtered_out, + dropped, + font, + } + } + + fn total(&self) -> u128 { + self.incoming + self.outgoing + self.filtered_out + self.dropped + } + + fn title(&self) -> String { + let total = self.total(); + if self.chart_type.eq(&ChartType::Bytes) { + ByteMultiple::formatted_string(total) + } else { + total.to_string() + } + } + + fn angles(&self) -> [(Radians, Radians); 4] { + #[allow(clippy::cast_precision_loss)] + let mut values = [ + self.incoming as f32, + self.outgoing as f32, + self.filtered_out as f32, + self.dropped as f32, + ]; + let total: f32 = values.iter().sum(); + let min_val = 2.0 * total / 100.0; + let mut diff = 0.0; + + for value in &mut values { + if *value > 0.0 && *value < min_val { + diff += min_val - *value; + *value = min_val; + } + } + // remove the diff from the max value + if diff > 0.0 { + let _ = values + .iter_mut() + .max_by(|a, b| a.total_cmp(b)) + .map(|max| *max -= diff); + } + + let mut start_angle = Radians(-consts::FRAC_PI_2); + values.map(|value| { + let start = start_angle; + let end = start + Radians(consts::TAU) * value / total; + start_angle = end; + (start, end) + }) + } +} + +impl canvas::Program for DonutChart { + type State = (); + + fn draw( + &self, + (): &Self::State, + renderer: &Renderer, + theme: &Theme, + bounds: iced::Rectangle, + _: mouse::Cursor, + ) -> Vec { + let mut frame = Frame::new(renderer, bounds.size()); + let center = frame.center(); + let radius = (frame.width().min(frame.height()) / 2.0) * 0.9; + + let style = ::style(theme, &::default()); + let colors = [ + style.incoming, + style.outgoing, + style.filtered_out, + style.dropped, + ]; + + for ((start_angle, end_angle), color) in self.angles().into_iter().zip(colors) { + let path = canvas::Path::new(|builder| { + builder.arc(Arc { + center, + radius, + start_angle, + end_angle, + }); + builder.line_to(center); + builder.close(); + }); + + frame.fill(&path, color); + } + + let inner_circle = canvas::Path::circle(center, radius - 6.0); + frame.fill(&inner_circle, style.background); + frame.fill_text(Text { + content: self.title().clone(), + position: center, + vertical_alignment: Vertical::Center, + horizontal_alignment: Horizontal::Center, + color: style.text_color, + size: FONT_SIZE_SUBTITLE.into(), + font: self.font, + ..Default::default() + }); + + vec![frame.into_geometry()] + } +} + +pub fn donut_chart( + chart_type: ChartType, + incoming: u128, + outgoing: u128, + filtered_out: u128, + dropped: u128, + font: Font, +) -> Canvas { + iced::widget::canvas(DonutChart::new( + chart_type, + incoming, + outgoing, + filtered_out, + dropped, + font, + )) + .width(110) + .height(110) +} diff --git a/src/chart/types/mod.rs b/src/chart/types/mod.rs index 22966b922..ca3f7e1bd 100644 --- a/src/chart/types/mod.rs +++ b/src/chart/types/mod.rs @@ -1,2 +1,3 @@ pub mod chart_type; +pub mod donut_chart; pub mod traffic_chart; diff --git a/src/gui/pages/inspect_page.rs b/src/gui/pages/inspect_page.rs index 13cb7c145..c50cfa6bd 100644 --- a/src/gui/pages/inspect_page.rs +++ b/src/gui/pages/inspect_page.rs @@ -14,7 +14,7 @@ use iced::{alignment, Alignment, Font, Length, Padding, Pixels}; use crate::chart::types::chart_type::ChartType; use crate::gui::components::tab::get_pages_tabs; use crate::gui::components::types::my_modal::MyModal; -use crate::gui::pages::overview_page::get_bars; +use crate::gui::pages::overview_page::{get_bars, get_bars_length}; use crate::gui::styles::button::ButtonType; use crate::gui::styles::container::ContainerType; use crate::gui::styles::scrollbar::ScrollbarType; @@ -24,7 +24,7 @@ use crate::gui::styles::text_input::TextInputType; use crate::gui::types::message::Message; use crate::networking::types::address_port_pair::AddressPortPair; use crate::networking::types::byte_multiple::ByteMultiple; -use crate::networking::types::data_info::DataInfoWithoutTimestamp; +use crate::networking::types::data_info::DataInfo; use crate::networking::types::host_data_states::HostStates; use crate::networking::types::info_address_port_pair::InfoAddressPortPair; use crate::networking::types::traffic_direction::TrafficDirection; @@ -559,20 +559,14 @@ fn get_button_change_page<'a>(increment: bool) -> Button<'a, Message, StyleType> fn get_agglomerates_row<'a>( font: Font, - tot: DataInfoWithoutTimestamp, + tot: DataInfo, chart_type: ChartType, ) -> Row<'a, Message, StyleType> { - let tot_packets = tot.incoming_packets + tot.outgoing_packets; - let tot_bytes = tot.incoming_bytes + tot.outgoing_bytes; + let tot_packets = tot.tot_packets(); + let tot_bytes = tot.tot_bytes(); let width = ReportCol::FILTER_COLUMNS_WIDTH; - #[allow(clippy::cast_precision_loss)] - let in_length = if chart_type == ChartType::Packets { - width * (tot.incoming_packets as f32 / tot_packets as f32) - } else { - width * (tot.incoming_bytes as f32 / tot_bytes as f32) - }; - let out_length = width - in_length; + let (in_length, out_length) = get_bars_length(width, chart_type, &tot, &tot); let bars = get_bars(in_length, out_length); let bytes_col = Column::new() diff --git a/src/gui/pages/overview_page.rs b/src/gui/pages/overview_page.rs index 16a401af2..3ab874cd1 100644 --- a/src/gui/pages/overview_page.rs +++ b/src/gui/pages/overview_page.rs @@ -3,16 +3,7 @@ //! It contains elements to display traffic statistics: chart, detailed connections data //! and overall statistics about the filtered traffic. -use iced::widget::scrollable::Direction; -use iced::widget::text::LineHeight; -use iced::widget::tooltip::Position; -use iced::widget::{ - button, horizontal_space, lazy, vertical_space, Button, Column, Container, Row, Rule, - Scrollable, Space, Text, Tooltip, -}; -use iced::Length::{Fill, FillPortion}; -use iced::{Alignment, Font, Length, Padding}; - +use crate::chart::types::donut_chart::donut_chart; use crate::countries::country_utils::get_flag_tooltip; use crate::countries::flags_pictures::FLAGS_WIDTH_BIG; use crate::gui::components::tab::get_pages_tabs; @@ -33,19 +24,30 @@ use crate::report::get_report_entries::{get_host_entries, get_service_entries}; use crate::report::types::search_parameters::SearchParameters; use crate::report::types::sort_type::SortType; use crate::translations::translations::{ - active_filters_translation, bytes_chart_translation, error_translation, - filtered_bytes_translation, filtered_packets_translation, network_adapter_translation, - no_addresses_translation, none_translation, of_total_translation, packets_chart_translation, - some_observed_translation, traffic_rate_translation, waiting_translation, + active_filters_translation, bytes_chart_translation, error_translation, incoming_translation, + network_adapter_translation, no_addresses_translation, none_translation, outgoing_translation, + packets_chart_translation, some_observed_translation, traffic_rate_translation, + waiting_translation, }; use crate::translations::translations_2::{ - data_representation_translation, dropped_packets_translation, host_translation, + data_representation_translation, dropped_translation, host_translation, only_top_30_items_translation, }; use crate::translations::translations_3::{service_translation, unsupported_link_type_translation}; -use crate::utils::formatted_strings::{get_active_filters_string, get_percentage_string}; +use crate::translations::translations_4::excluded_translation; +use crate::utils::formatted_strings::get_active_filters_string; use crate::utils::types::icon::Icon; use crate::{ByteMultiple, ChartType, ConfigSettings, Language, RunningPage, StyleType}; +use iced::alignment::{Horizontal, Vertical}; +use iced::widget::scrollable::Direction; +use iced::widget::text::LineHeight; +use iced::widget::tooltip::Position; +use iced::widget::{ + button, horizontal_space, lazy, vertical_space, Button, Column, Container, Row, Rule, + Scrollable, Space, Text, Tooltip, +}; +use iced::Length::{Fill, FillPortion}; +use iced::{Alignment, Font, Length, Padding, Shrink}; /// Computes the body of gui overview page pub fn overview_page(sniffer: &Sniffer) -> Container { @@ -72,14 +74,8 @@ pub fn overview_page(sniffer: &Sniffer) -> Container { } (observed, 0) => { //no packets have been filtered but some have been observed - body = body_no_observed( - &sniffer.filters, - observed, - font, - font_headers, - language, - &sniffer.waiting, - ); + body = + body_no_observed(&sniffer.filters, observed, font, language, &sniffer.waiting); } (_observed, filtered) => { //observed > filtered > 0 || observed = filtered > 0 @@ -96,7 +92,7 @@ pub fn overview_page(sniffer: &Sniffer) -> Container { let container_info = lazy( (total, style, language, sniffer.traffic_chart.chart_type), - move |_| lazy_col_info(total, filtered, dropped, sniffer), + move |_| lazy_col_info(sniffer), ); let num_favorites = sniffer.info_traffic.lock().unwrap().favorite_hosts.len(); @@ -120,8 +116,8 @@ pub fn overview_page(sniffer: &Sniffer) -> Container { .align_x(Alignment::Center) .push( Row::new() + .height(280) .spacing(10) - .height(Fill) .push(container_info) .push(container_chart), ) @@ -190,7 +186,6 @@ fn body_no_observed<'a>( filters: &Filters, observed: u128, font: Font, - font_headers: Font, language: Language, waiting: &str, ) -> Column<'a, Message, StyleType> { @@ -205,13 +200,7 @@ fn body_no_observed<'a>( .align_x(Alignment::Center) .push(vertical_space()) .push(Icon::Funnel.to_text().size(60)) - .push(get_active_filters_col( - filters, - language, - font, - font_headers, - true, - )) + .push(get_active_filters_col(filters, language, font)) .push(Rule::horizontal(20)) .push(tot_packets_text) .push(Text::new(waiting.to_owned()).font(font).size(50)) @@ -253,7 +242,7 @@ fn lazy_row_report<'a>(sniffer: &Sniffer) -> Container<'a, Message, StyleType> { .push(col_service); Container::new(row_report) - .height(Fill) + .height(Shrink) .class(ContainerType::BorderedRound) } @@ -446,40 +435,25 @@ fn col_service<'a>(width: f32, sniffer: &Sniffer) -> Column<'a, Message, StyleTy ) } -fn lazy_col_info<'a>( - total: u128, - filtered: u128, - dropped: u32, - sniffer: &Sniffer, -) -> Container<'a, Message, StyleType> { +fn lazy_col_info<'a>(sniffer: &Sniffer) -> Container<'a, Message, StyleType> { let ConfigSettings { style, language, .. } = sniffer.configs.lock().unwrap().settings; - let PaletteExtension { - font, font_headers, .. - } = style.get_extension(); + let PaletteExtension { font, .. } = style.get_extension(); let col_device = col_device(language, font, &sniffer.device); let col_data_representation = col_data_representation(language, font, sniffer.traffic_chart.chart_type); - let col_bytes_packets = col_bytes_packets( - language, - dropped, - total, - filtered, - font, - font_headers, - sniffer, - ); + let donut_row = donut_row(language, font, sniffer); let content = Column::new() .align_x(Alignment::Center) .padding([5, 10]) .push( Row::new() - .height(120) + .height(Length::Fill) .push( Scrollable::with_direction( col_device, @@ -491,14 +465,7 @@ fn lazy_col_info<'a>( .push(col_data_representation.width(Length::Fill)), ) .push(Rule::horizontal(15)) - .push( - Scrollable::with_direction( - col_bytes_packets, - Direction::Vertical(ScrollbarType::properties()), - ) - .height(Length::Fill) - .width(Length::Fill), - ); + .push(donut_row.height(Length::Fill)); Container::new(content) .width(400) @@ -603,71 +570,140 @@ fn col_data_representation<'a>( ret_val } -fn col_bytes_packets<'a>( +fn donut_row<'a>( language: Language, - dropped: u32, - total: u128, - filtered: u128, font: Font, - font_headers: Font, sniffer: &Sniffer, -) -> Column<'a, Message, StyleType> { - let filtered_bytes = sniffer.runtime_data.tot_out_bytes + sniffer.runtime_data.tot_in_bytes; - let all_bytes = sniffer.runtime_data.all_bytes; +) -> Container<'a, Message, StyleType> { + let chart_type = sniffer.traffic_chart.chart_type; let filters = &sniffer.filters; - let dropped_val = if dropped > 0 { - format!( - "{} {}", - dropped, - of_total_translation(language, &get_percentage_string(total, u128::from(dropped))) + let (in_data, out_data, filtered_out, dropped) = if chart_type.eq(&ChartType::Bytes) { + ( + sniffer.runtime_data.tot_in_bytes, + sniffer.runtime_data.tot_out_bytes, + sniffer.runtime_data.all_bytes + - sniffer.runtime_data.tot_out_bytes + - sniffer.runtime_data.tot_in_bytes, + // assume that the dropped packets have the same size as the average packet + u128::from(sniffer.runtime_data.dropped_packets) * sniffer.runtime_data.all_bytes + / sniffer.runtime_data.all_packets, ) } else { - none_translation(language).to_string() - }; - let bytes_value = if dropped > 0 { - ByteMultiple::formatted_string(filtered_bytes) - } else { - format!( - "{} {}", - &ByteMultiple::formatted_string(filtered_bytes), - of_total_translation(language, &get_percentage_string(all_bytes, filtered_bytes)) + ( + sniffer.runtime_data.tot_in_packets, + sniffer.runtime_data.tot_out_packets, + sniffer.runtime_data.all_packets + - sniffer.runtime_data.tot_out_packets + - sniffer.runtime_data.tot_in_packets, + u128::from(sniffer.runtime_data.dropped_packets), ) }; - Column::new() - .spacing(10) - .push(get_active_filters_col( + let legend_entry_filtered = if filters.none_active() { + None + } else { + Some(donut_legend_entry( + filtered_out, + chart_type, + RuleType::FilteredOut, filters, - language, font, - font_headers, - false, + language, )) - .push(TextType::highlighted_subtitle_with_desc( - filtered_bytes_translation(language), - &bytes_value, + }; + + let legend_col = Column::new() + .spacing(5) + .push(donut_legend_entry( + in_data, + chart_type, + RuleType::Incoming, + filters, font, + language, )) - .push(TextType::highlighted_subtitle_with_desc( - filtered_packets_translation(language), - &format!( - "{} {}", - filtered, - of_total_translation(language, &get_percentage_string(total, filtered)) - ), + .push(donut_legend_entry( + out_data, + chart_type, + RuleType::Outgoing, + filters, font, + language, )) - .push(TextType::highlighted_subtitle_with_desc( - dropped_packets_translation(language), - &dropped_val, + .push_maybe(legend_entry_filtered) + .push(donut_legend_entry( + dropped, + chart_type, + RuleType::Dropped, + filters, + font, + language, + )); + + let donut_row = Row::new() + .align_y(Vertical::Center) + .spacing(20) + .push(donut_chart( + chart_type, + in_data, + out_data, + filtered_out, + dropped, font, )) + .push(legend_col); + + Container::new(donut_row) + .height(Length::Fill) + .width(Length::Fill) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) +} + +fn donut_legend_entry<'a>( + value: u128, + chart_type: ChartType, + rule_type: RuleType, + filters: &Filters, + font: Font, + language: Language, +) -> Row<'a, Message, StyleType> { + let value_text = if chart_type.eq(&ChartType::Bytes) { + ByteMultiple::formatted_string(value) + } else { + value.to_string() + }; + + let label = match rule_type { + RuleType::Incoming => incoming_translation(language), + RuleType::Outgoing => outgoing_translation(language), + RuleType::FilteredOut => excluded_translation(language), + RuleType::Dropped => dropped_translation(language), + _ => "", + }; + + let tooltip = if matches!(rule_type, RuleType::FilteredOut) { + Some(get_active_filters_tooltip(filters, language, font)) + } else { + None + }; + + Row::new() + .spacing(10) + .align_y(Alignment::Center) + .push( + Row::new() + .width(10) + .push(Rule::horizontal(1).class(rule_type)), + ) + .push(Text::new(format!("{label}: {value_text}")).font(font)) + .push_maybe(tooltip) } const MIN_BARS_LENGTH: f32 = 10.0; -fn get_bars_length( +pub fn get_bars_length( tot_width: f32, chart_type: ChartType, first_entry: &DataInfo, @@ -777,11 +813,9 @@ fn get_active_filters_col<'a>( filters: &Filters, language: Language, font: Font, - font_headers: Font, - show: bool, ) -> Column<'a, Message, StyleType> { let mut ret_val = Column::new().push( - Text::new(format!("{}:", active_filters_translation(language),)) + Text::new(active_filters_translation(language)) .font(font) .class(TextType::Subtitle), ); @@ -790,32 +824,46 @@ fn get_active_filters_col<'a>( ret_val = ret_val.push(Text::new(format!(" {}", none_translation(language))).font(font)); } else { let filters_string = get_active_filters_string(filters, language); - ret_val = ret_val.push(if show { - Row::new().push(Text::new(filters_string).font(font)) - } else { - Row::new().padding(Padding::ZERO.left(20)).push( - Tooltip::new( - Container::new( - Text::new("i") - .font(font_headers) - .size(15) - .line_height(LineHeight::Relative(1.0)), - ) - .align_x(Alignment::Center) - .align_y(Alignment::Center) - .height(20) - .width(20) - .class(ContainerType::Highlighted), - Text::new(filters_string).font(font), - Position::FollowCursor, - ) - .class(ContainerType::Tooltip), - ) - }); + ret_val = ret_val.push(Row::new().push(Text::new(filters_string).font(font))); } ret_val } +fn get_active_filters_tooltip<'a>( + filters: &Filters, + language: Language, + font: Font, +) -> Tooltip<'a, Message, StyleType> { + let filters_string = get_active_filters_string(filters, language); + + let mut ret_val = Column::new().push( + Text::new(active_filters_translation(language)) + .font(font) + .class(TextType::Subtitle), + ); + + ret_val = ret_val.push(Row::new().push(Text::new(filters_string).font(font))); + + let tooltip = Tooltip::new( + Container::new( + Text::new("i") + .font(font) + .size(15) + .line_height(LineHeight::Relative(1.0)), + ) + .align_x(Alignment::Center) + .align_y(Alignment::Center) + .height(20) + .width(20) + .class(ContainerType::BadgeInfo), + ret_val, + Position::FollowCursor, + ) + .class(ContainerType::Tooltip); + + tooltip +} + fn sort_arrows<'a>( active_sort_type: SortType, message: fn(SortType) -> Message, diff --git a/src/gui/styles/button.rs b/src/gui/styles/button.rs index 1cb6e63d5..a0b9a971a 100644 --- a/src/gui/styles/button.rs +++ b/src/gui/styles/button.rs @@ -7,7 +7,7 @@ use iced::widget::button; use iced::widget::button::{Catalog, Status, Style}; use iced::{Background, Border, Color, Shadow, Vector}; -use crate::gui::styles::style_constants::{BORDER_BUTTON_RADIUS, BORDER_WIDTH}; +use crate::gui::styles::style_constants::{ALERT_RED_COLOR, BORDER_BUTTON_RADIUS, BORDER_WIDTH}; use crate::gui::styles::types::gradient_type::{ get_gradient_buttons, get_gradient_hovered_buttons, GradientType, }; @@ -81,7 +81,7 @@ impl ButtonType { _ => BORDER_WIDTH, }, color: match self { - ButtonType::Alert => Color::new(0.8, 0.15, 0.15, 1.0), + ButtonType::Alert => ALERT_RED_COLOR, ButtonType::BorderedRound => Color { a: ext.alpha_round_borders, ..ext.buttons_color @@ -168,7 +168,7 @@ impl ButtonType { _ => BORDER_WIDTH, }, color: match self { - ButtonType::Alert => Color::new(0.8, 0.15, 0.15, 1.0), + ButtonType::Alert => ALERT_RED_COLOR, ButtonType::BorderedRound | ButtonType::NotStarred => Color { a: ext.alpha_round_borders, ..ext.buttons_color diff --git a/src/gui/styles/container.rs b/src/gui/styles/container.rs index d3cbbaeab..2cf6e46ee 100644 --- a/src/gui/styles/container.rs +++ b/src/gui/styles/container.rs @@ -17,6 +17,7 @@ pub enum ContainerType { BorderedRound, Tooltip, Badge, + BadgeInfo, Palette, Gradient(GradientType), Modal, @@ -43,7 +44,7 @@ impl ContainerType { a: ext.alpha_round_containers, ..ext.buttons_color }), - ContainerType::Badge => Background::Color(Color { + ContainerType::Badge | ContainerType::BadgeInfo => Background::Color(Color { a: ext.alpha_chart_badge, ..colors.secondary }), @@ -67,6 +68,7 @@ impl ContainerType { ContainerType::Modal => Radius::new(0).bottom(BORDER_ROUNDED_RADIUS), ContainerType::Tooltip => 7.0.into(), ContainerType::Badge + | ContainerType::BadgeInfo | ContainerType::Highlighted | ContainerType::HighlightedOnHeader => 100.0.into(), _ => 0.0.into(), @@ -83,6 +85,7 @@ impl ContainerType { }, color: match self { ContainerType::Palette => Color::BLACK, + ContainerType::BadgeInfo => colors.secondary, _ => Color { a: ext.alpha_round_borders, ..ext.buttons_color diff --git a/src/gui/styles/donut.rs b/src/gui/styles/donut.rs new file mode 100644 index 000000000..305034672 --- /dev/null +++ b/src/gui/styles/donut.rs @@ -0,0 +1,62 @@ +use crate::gui::styles::style_constants::ALERT_RED_COLOR; +use crate::gui::styles::types::style_type::StyleType; +use iced::Color; + +#[derive(Default)] +pub enum DonutType { + #[default] + Standard, +} + +impl DonutType { + #[allow(clippy::unused_self)] + fn active(&self, style: &StyleType) -> Style { + let colors = style.get_palette(); + let ext = style.get_extension(); + let primary = colors.primary; + let buttons = ext.buttons_color; + let background = Color { + r: primary.r + (buttons.r - primary.r) * ext.alpha_round_containers, + g: primary.g + (buttons.g - primary.g) * ext.alpha_round_containers, + b: primary.b + (buttons.b - primary.b) * ext.alpha_round_containers, + a: 1.0, + }; + Style { + background, + incoming: colors.secondary, + outgoing: colors.outgoing, + text_color: colors.text_body, + filtered_out: ext.buttons_color, + dropped: ALERT_RED_COLOR, + } + } +} + +impl Catalog for StyleType { + type Class<'a> = DonutType; + + fn default<'a>() -> Self::Class<'a> { + Self::Class::default() + } + + fn style(&self, class: &Self::Class<'_>) -> Style { + class.active(self) + } +} + +pub struct Style { + pub(crate) background: Color, + pub(crate) text_color: Color, + pub(crate) incoming: Color, + pub(crate) outgoing: Color, + pub(crate) filtered_out: Color, + pub(crate) dropped: Color, +} + +pub trait Catalog: Sized { + type Class<'a>; + + fn default<'a>() -> Self::Class<'a>; + + fn style(&self, class: &Self::Class<'_>) -> Style; +} diff --git a/src/gui/styles/mod.rs b/src/gui/styles/mod.rs index 833ffa33b..c13ac30f8 100644 --- a/src/gui/styles/mod.rs +++ b/src/gui/styles/mod.rs @@ -3,6 +3,7 @@ pub mod checkbox; pub mod combobox; pub mod container; pub mod custom_themes; +pub mod donut; pub mod picklist; pub mod radio; pub mod rule; diff --git a/src/gui/styles/rule.rs b/src/gui/styles/rule.rs index 9ce67a43b..fec2aa6d6 100644 --- a/src/gui/styles/rule.rs +++ b/src/gui/styles/rule.rs @@ -2,11 +2,11 @@ #![allow(clippy::module_name_repetitions)] +use crate::gui::styles::style_constants::ALERT_RED_COLOR; +use crate::StyleType; use iced::widget::rule::{Catalog, FillMode, Style}; use iced::Color; -use crate::StyleType; - #[derive(Default)] pub enum RuleType { #[default] @@ -14,6 +14,8 @@ pub enum RuleType { PaletteColor(Color, u16), Incoming, Outgoing, + FilteredOut, + Dropped, } impl RuleType { @@ -25,15 +27,17 @@ impl RuleType { RuleType::Incoming => colors.secondary, RuleType::Outgoing => colors.outgoing, RuleType::PaletteColor(color, _) => *color, + RuleType::Dropped => ALERT_RED_COLOR, + RuleType::FilteredOut => ext.buttons_color, RuleType::Standard => Color { a: ext.alpha_round_borders, ..ext.buttons_color }, }, width: match self { - RuleType::Incoming | RuleType::Outgoing => 5, RuleType::PaletteColor(_, width) => *width, RuleType::Standard => 3, + _ => 5, }, radius: 0.0.into(), fill_mode: FillMode::Full, diff --git a/src/gui/styles/style_constants.rs b/src/gui/styles/style_constants.rs index acb32b098..b1c15da79 100644 --- a/src/gui/styles/style_constants.rs +++ b/src/gui/styles/style_constants.rs @@ -223,3 +223,11 @@ pub const BORDER_WIDTH: f32 = 2.0; pub const CHARTS_LINE_BORDER: u32 = 1; pub const BORDER_ROUNDED_RADIUS: f32 = 15.0; pub const BORDER_BUTTON_RADIUS: f32 = 180.0; + +// red color for alerts +pub const ALERT_RED_COLOR: Color = Color { + r: 0.8, + g: 0.15, + b: 0.15, + a: 1.0, +}; diff --git a/src/gui/styles/text.rs b/src/gui/styles/text.rs index 1a1a2485a..f9b0d7750 100644 --- a/src/gui/styles/text.rs +++ b/src/gui/styles/text.rs @@ -2,13 +2,13 @@ #![allow(clippy::module_name_repetitions)] +use crate::gui::styles::style_constants::ALERT_RED_COLOR; +use crate::gui::types::message::Message; +use crate::StyleType; use iced::widget::text::{Catalog, Style}; use iced::widget::{Column, Text}; use iced::{Color, Font}; -use crate::gui::types::message::Message; -use crate::StyleType; - #[derive(Copy, Clone, Default, PartialEq)] pub enum TextType { #[default] @@ -74,7 +74,7 @@ pub fn highlight(style: &StyleType, element: TextType) -> Color { } TextType::Incoming => colors.secondary, TextType::Outgoing => colors.outgoing, - TextType::Danger => Color::from_rgb(0.8, 0.15, 0.15), + TextType::Danger => ALERT_RED_COLOR, TextType::Sponsor => Color::from_rgb(1.0, 0.3, 0.5), TextType::Standard => colors.text_body, TextType::Starred => colors.starred, diff --git a/src/gui/styles/text_input.rs b/src/gui/styles/text_input.rs index b49215715..35512e05e 100644 --- a/src/gui/styles/text_input.rs +++ b/src/gui/styles/text_input.rs @@ -5,7 +5,7 @@ use iced::widget::text_input::{Catalog, Status, Style}; use iced::{Background, Border, Color}; -use crate::gui::styles::style_constants::BORDER_WIDTH; +use crate::gui::styles::style_constants::{ALERT_RED_COLOR, BORDER_WIDTH}; use crate::StyleType; #[derive(Default)] @@ -36,7 +36,7 @@ impl TextInputType { color: match self { TextInputType::Badge => Color::TRANSPARENT, TextInputType::Standard => ext.buttons_color, - TextInputType::Error => Color::new(0.8, 0.15, 0.15, 1.0), + TextInputType::Error => ALERT_RED_COLOR, }, }, icon: Color { @@ -58,7 +58,7 @@ impl TextInputType { radius: TEXT_INPUT_BORDER_RADIUS.into(), width: BORDER_WIDTH, color: match self { - TextInputType::Error => Color::new(0.8, 0.15, 0.15, 1.0), + TextInputType::Error => ALERT_RED_COLOR, _ => colors.secondary, }, }, @@ -119,7 +119,7 @@ impl TextInputType { radius: TEXT_INPUT_BORDER_RADIUS.into(), width: BORDER_WIDTH, color: match self { - TextInputType::Error => Color::new(0.8, 0.15, 0.15, 1.0), + TextInputType::Error => ALERT_RED_COLOR, _ => colors.secondary, }, }, @@ -153,7 +153,10 @@ impl TextInputType { a: ext.alpha_round_borders, ..ext.buttons_color }, - TextInputType::Error => Color::new(0.8, 0.15, 0.15, ext.alpha_round_borders), + TextInputType::Error => Color { + a: ext.alpha_round_borders, + ..ALERT_RED_COLOR + }, }, }, icon: Color { diff --git a/src/main.rs b/src/main.rs index e5d1bb0d0..7094db302 100644 --- a/src/main.rs +++ b/src/main.rs @@ -114,7 +114,7 @@ pub fn main() -> iced::Result { ], default_font: Font::with_name(FONT_FAMILY_NAME), default_text_size: Pixels(FONT_SIZE_BODY), - antialiasing: false, + antialiasing: true, }) .window(window::Settings { size: size.to_size(), // start size diff --git a/src/networking/types/data_info.rs b/src/networking/types/data_info.rs index 766cc3405..7ddd3a4bc 100644 --- a/src/networking/types/data_info.rs +++ b/src/networking/types/data_info.rs @@ -61,6 +61,17 @@ impl DataInfo { self.final_timestamp = Local::now(); } + pub fn add_packets(&mut self, packets: u128, bytes: u128, traffic_direction: TrafficDirection) { + if traffic_direction.eq(&TrafficDirection::Outgoing) { + self.outgoing_packets += packets; + self.outgoing_bytes += bytes; + } else { + self.incoming_packets += packets; + self.incoming_bytes += bytes; + } + self.final_timestamp = Local::now(); + } + pub fn new_with_first_packet(bytes: u128, traffic_direction: TrafficDirection) -> Self { if traffic_direction.eq(&TrafficDirection::Outgoing) { Self { @@ -122,15 +133,3 @@ impl AddAssign for DataInfo { self.final_timestamp = Local::now(); } } - -#[derive(Clone, Default, Copy)] -pub struct DataInfoWithoutTimestamp { - /// Incoming packets - pub incoming_packets: u128, - /// Outgoing packets - pub outgoing_packets: u128, - /// Incoming bytes - pub incoming_bytes: u128, - /// Outgoing bytes - pub outgoing_bytes: u128, -} diff --git a/src/notifications/types/notifications.rs b/src/notifications/types/notifications.rs index 1705550df..e63330dcc 100644 --- a/src/notifications/types/notifications.rs +++ b/src/notifications/types/notifications.rs @@ -185,7 +185,7 @@ mod tests { previous_threshold: 500_000, threshold: Some(500_000),byte_multiple: ByteMultiple::KB, ..BytesNotification::default() })] #[case("420m", BytesNotification { previous_threshold: 420_000_000, threshold: Some(420_000_000),byte_multiple: ByteMultiple::MB, ..BytesNotification::default() })] - #[case("744Ь", BytesNotification { + #[case("744ь", BytesNotification { previous_threshold: 744, threshold: Some(744),byte_multiple: ByteMultiple::B, ..BytesNotification::default() })] #[case("888g", BytesNotification { previous_threshold: 888_000_000_000, threshold: Some(888_000_000_000),byte_multiple: ByteMultiple::GB, ..BytesNotification::default() })] diff --git a/src/report/get_report_entries.rs b/src/report/get_report_entries.rs index 893f12375..62b465515 100644 --- a/src/report/get_report_entries.rs +++ b/src/report/get_report_entries.rs @@ -3,11 +3,10 @@ use std::sync::Mutex; use crate::networking::manage_packets::get_address_to_lookup; use crate::networking::types::address_port_pair::AddressPortPair; -use crate::networking::types::data_info::{DataInfo, DataInfoWithoutTimestamp}; +use crate::networking::types::data_info::DataInfo; use crate::networking::types::data_info_host::DataInfoHost; use crate::networking::types::host::Host; use crate::networking::types::info_address_port_pair::InfoAddressPortPair; -use crate::networking::types::traffic_direction::TrafficDirection; use crate::report::types::sort_type::SortType; use crate::{ChartType, InfoTraffic, ReportSortType, Service, Sniffer}; @@ -16,12 +15,8 @@ use crate::{ChartType, InfoTraffic, ReportSortType, Service, Sniffer}; /// with their packets, in-bytes, and out-bytes count pub fn get_searched_entries( sniffer: &Sniffer, -) -> ( - Vec<(AddressPortPair, InfoAddressPortPair)>, - usize, - DataInfoWithoutTimestamp, -) { - let mut agglomerate = DataInfoWithoutTimestamp::default(); +) -> (Vec<(AddressPortPair, InfoAddressPortPair)>, usize, DataInfo) { + let mut agglomerate = DataInfo::default(); let info_traffic_lock = sniffer.info_traffic.lock().unwrap(); let mut all_results: Vec<(&AddressPortPair, &InfoAddressPortPair)> = info_traffic_lock .map @@ -39,13 +34,11 @@ pub fn get_searched_entries( .match_entry(key, value, r_dns_host, is_favorite) }) .map(|(key, val)| { - if val.traffic_direction == TrafficDirection::Outgoing { - agglomerate.outgoing_packets += val.transmitted_packets; - agglomerate.outgoing_bytes += val.transmitted_bytes; - } else { - agglomerate.incoming_packets += val.transmitted_packets; - agglomerate.incoming_bytes += val.transmitted_bytes; - } + agglomerate.add_packets( + val.transmitted_packets, + val.transmitted_bytes, + val.traffic_direction, + ); (key, val) }) .collect(); diff --git a/src/translations/translations.rs b/src/translations/translations.rs index f47adedd3..3b963e128 100644 --- a/src/translations/translations.rs +++ b/src/translations/translations.rs @@ -662,93 +662,93 @@ pub fn some_observed_translation<'a>(language: Language, observed: u128) -> Text }) } -pub fn filtered_packets_translation(language: Language) -> &'static str { - match language { - Language::EN => "Filtered packets", - Language::IT => "Pacchetti filtrati", - Language::FR => "Paquets filtrés", - Language::ES => "Paquetes filtrados", - Language::PL => "Przefiltrowane pakiety", - Language::DE => "Gefilterte Pakete", - Language::UK => "Відфільтровані пакети", - Language::ZH => "目标数据包计数", - Language::RO => "Pachete filtrate", - Language::KO => "필터링된 패킷", - Language::TR => "Filtrelenen paketler", - Language::RU => "Отфильтровано пакетов", - Language::PT => "Pacotes filtrados", - Language::EL => "Φιλτραρισμένα πακέτα", - // Language::FA => "بسته های صاف شده", - Language::SV => "Filtrerade paket", - Language::FI => "Suodatettuja paketteja", - Language::JA => "フィルタリングされたパケット", - Language::UZ => "Filtrlangan paketlar", - Language::VI => "Các gói tin đã được lọc", - } -} +// pub fn filtered_packets_translation(language: Language) -> &'static str { +// match language { +// Language::EN => "Filtered packets", +// Language::IT => "Pacchetti filtrati", +// Language::FR => "Paquets filtrés", +// Language::ES => "Paquetes filtrados", +// Language::PL => "Przefiltrowane pakiety", +// Language::DE => "Gefilterte Pakete", +// Language::UK => "Відфільтровані пакети", +// Language::ZH => "目标数据包计数", +// Language::RO => "Pachete filtrate", +// Language::KO => "필터링된 패킷", +// Language::TR => "Filtrelenen paketler", +// Language::RU => "Отфильтровано пакетов", +// Language::PT => "Pacotes filtrados", +// Language::EL => "Φιλτραρισμένα πακέτα", +// // Language::FA => "بسته های صاف شده", +// Language::SV => "Filtrerade paket", +// Language::FI => "Suodatettuja paketteja", +// Language::JA => "フィルタリングされたパケット", +// Language::UZ => "Filtrlangan paketlar", +// Language::VI => "Các gói tin đã được lọc", +// } +// } -pub fn filtered_bytes_translation(language: Language) -> &'static str { - match language { - Language::EN => "Filtered bytes", - Language::IT => "Byte filtrati", - Language::FR => "Octets filtrés", - Language::ES | Language::PT => "Bytes filtrados", - Language::PL => "Przechwycone bajty", - Language::DE => "Gefilterte Bytes", - Language::UK => "Відфільтровані байти", - Language::ZH => "目标网络流量计数", - Language::RO => "Octeți filtrați", - Language::KO => "필터링된 바이트", - Language::TR => "Filtrelenen bayt", - Language::RU => "Отфильтровано байт", - Language::EL => "Φιλτραρισμένα bytes", - // Language::FA => "بایت های صاف شده", - Language::SV => "Filtrerade bytes", - Language::FI => "Suodatettuja tavuja", - Language::JA => "フィルタリングされたバイト", - Language::UZ => "Filtrlangan baytlar", - Language::VI => "Các bytes đã được lọc", - } -} +// pub fn filtered_bytes_translation(language: Language) -> &'static str { +// match language { +// Language::EN => "Filtered bytes", +// Language::IT => "Byte filtrati", +// Language::FR => "Octets filtrés", +// Language::ES | Language::PT => "Bytes filtrados", +// Language::PL => "Przechwycone bajty", +// Language::DE => "Gefilterte Bytes", +// Language::UK => "Відфільтровані байти", +// Language::ZH => "目标网络流量计数", +// Language::RO => "Octeți filtrați", +// Language::KO => "필터링된 바이트", +// Language::TR => "Filtrelenen bayt", +// Language::RU => "Отфильтровано байт", +// Language::EL => "Φιλτραρισμένα bytes", +// // Language::FA => "بایت های صاف شده", +// Language::SV => "Filtrerade bytes", +// Language::FI => "Suodatettuja tavuja", +// Language::JA => "フィルタリングされたバイト", +// Language::UZ => "Filtrlangan baytlar", +// Language::VI => "Các bytes đã được lọc", +// } +// } -pub fn of_total_translation(language: Language, percentage: &str) -> String { - match language { - Language::EN => format!("({percentage} of the total)"), - Language::IT => format!("({percentage} del totale)"), - Language::FR => format!("({percentage} du total)"), - Language::ES => format!("({percentage} del total)"), - Language::PL => format!("({percentage} z całości)"), - Language::DE => format!("({percentage} der Gesamtzahl)"), - Language::UK => { - format!("({percentage} від загальної суми)") - } - Language::ZH => { - format!("(占所有数据包的 {percentage})") - } - Language::RO => { - format!("({percentage} din total)") - } - Language::KO => { - format!("({percentage} 의 일부)") - } - Language::TR => format!("toplamın ({percentage})"), - Language::RU => { - format!("({percentage} от общего числа)") - } - Language::PT => { - format!("({percentage} do total)") - } - Language::EL => { - format!("({percentage} από τα συνολικά)") - } - // Language::FA => format!("({percentage} از مجموع)"), - Language::SV => format!("({percentage} av totalen)"), - Language::FI => format!("({percentage} kokonaismäärästä)"), - Language::JA => format!("(トータル: {percentage} )"), - Language::UZ => format!("(Jami: {percentage} )"), - Language::VI => format!("({percentage} trên tổng cộng)"), - } -} +// pub fn of_total_translation(language: Language, percentage: &str) -> String { +// match language { +// Language::EN => format!("({percentage} of the total)"), +// Language::IT => format!("({percentage} del totale)"), +// Language::FR => format!("({percentage} du total)"), +// Language::ES => format!("({percentage} del total)"), +// Language::PL => format!("({percentage} z całości)"), +// Language::DE => format!("({percentage} der Gesamtzahl)"), +// Language::UK => { +// format!("({percentage} від загальної суми)") +// } +// Language::ZH => { +// format!("(占所有数据包的 {percentage})") +// } +// Language::RO => { +// format!("({percentage} din total)") +// } +// Language::KO => { +// format!("({percentage} 의 일부)") +// } +// Language::TR => format!("toplamın ({percentage})"), +// Language::RU => { +// format!("({percentage} от общего числа)") +// } +// Language::PT => { +// format!("({percentage} do total)") +// } +// Language::EL => { +// format!("({percentage} από τα συνολικά)") +// } +// // Language::FA => format!("({percentage} از مجموع)"), +// Language::SV => format!("({percentage} av totalen)"), +// Language::FI => format!("({percentage} kokonaismäärästä)"), +// Language::JA => format!("(トータル: {percentage} )"), +// Language::UZ => format!("(Jami: {percentage} )"), +// Language::VI => format!("({percentage} trên tổng cộng)"), +// } +// } // pub fn filtered_application_translation(language: Language) -> Text { // Text::new(match language { @@ -1354,7 +1354,7 @@ pub fn incoming_translation(language: Language) -> &'static str { Language::DE => "Eingehend", Language::UK => "Вхідні", Language::ZH => "入站", - Language::RO => "de intrare", + Language::RO => "De intrare", Language::KO => "수신중", Language::TR => "Gelen", Language::RU => "Входящий", @@ -1379,7 +1379,7 @@ pub fn outgoing_translation(language: Language) -> &'static str { Language::DE => "Ausgehend", Language::UK => "Вихідні", Language::ZH => "出站", - Language::RO => "de ieșire", + Language::RO => "De ieșire", Language::KO => "발신중", Language::TR => "Giden", Language::RU => "Исходящий", diff --git a/src/translations/translations_2.rs b/src/translations/translations_2.rs index 0e2a2b98f..eeef8867b 100644 --- a/src/translations/translations_2.rs +++ b/src/translations/translations_2.rs @@ -77,28 +77,28 @@ pub fn connection_details_translation(language: Language) -> &'static str { } } -pub fn dropped_packets_translation(language: Language) -> &'static str { +// refers to bytes or packets dropped because they weren't processed fast enough +pub fn dropped_translation(language: Language) -> &'static str { match language { - Language::EN => "Dropped packets", - Language::IT => "Pacchetti mancati", - Language::RU => "Потеряно пакетов", - Language::SV => "Tappade paket", - Language::FI => "Pudotetut paketit", - Language::DE => "Verlorene Pakete", - Language::TR => "Düşen paketler", - // Language::FA => "بسته های رها شده", - Language::ES => "Paquetes perdidos", - Language::KO => "손실 패킷", - Language::ZH => "丢包计数", - Language::UK => "Пропущені пакети", - Language::RO => "Pachete pierdute", - Language::PL => "Utracone pakiety", - Language::FR => "Packets perdus", - Language::JA => "ドロップしたパケット", - Language::UZ => "Yig'ilgan paketlar", - Language::PT => "Pacotes perdidos", - Language::VI => "Gói tin đã bị mất", - _ => "Dropped packets", + Language::EN => "Dropped", + Language::IT => "Persi", + Language::RU => "Потеряно", + Language::SV => "Tappade", + Language::FI => "Pudotetut", + Language::DE => "Verlorene", + Language::TR => "Düşen", + // Language::FA => "رها شده", + Language::ES | Language::PT => "Perdidos", + Language::KO => "손실", + Language::ZH => "丢计", + Language::UK => "Пропущені", + Language::RO => "Pierdute", + Language::PL => "Utracone", + Language::FR => "Perdus", + Language::JA => "ドロップした", + Language::UZ => "Yig'ilgan", + Language::VI => "Mất", + _ => "Dropped", } } diff --git a/src/translations/translations_4.rs b/src/translations/translations_4.rs index 9c7319594..048ad7f46 100644 --- a/src/translations/translations_4.rs +++ b/src/translations/translations_4.rs @@ -19,3 +19,12 @@ pub fn share_feedback_translation(language: Language) -> &'static str { _ => "Share your feedback", } } + +// refers to bytes or packets excluded because of the filters +pub fn excluded_translation(language: Language) -> &'static str { + match language { + Language::EN => "Excluded", + Language::IT => "Esclusi", + _ => "Excluded", + } +} diff --git a/src/utils/formatted_strings.rs b/src/utils/formatted_strings.rs index 88c6bf932..af9c5ed37 100644 --- a/src/utils/formatted_strings.rs +++ b/src/utils/formatted_strings.rs @@ -11,18 +11,18 @@ use crate::Language; /// Application version number (to be displayed in gui footer) pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); -/// Computes the String representing the percentage of filtered bytes/packets -pub fn get_percentage_string(observed: u128, filtered: u128) -> String { - #[allow(clippy::cast_precision_loss)] - let filtered_float = filtered as f32; - #[allow(clippy::cast_precision_loss)] - let observed_float = observed as f32; - if format!("{:.1}", 100.0 * filtered_float / observed_float).eq("0.0") { - "<0.1%".to_string() - } else { - format!("{:.1}%", 100.0 * filtered_float / observed_float) - } -} +// /// Computes the String representing the percentage of filtered bytes/packets +// pub fn get_percentage_string(observed: u128, filtered: u128) -> String { +// #[allow(clippy::cast_precision_loss)] +// let filtered_float = filtered as f32; +// #[allow(clippy::cast_precision_loss)] +// let observed_float = observed as f32; +// if format!("{:.1}", 100.0 * filtered_float / observed_float).eq("0.0") { +// "<0.1%".to_string() +// } else { +// format!("{:.1}%", 100.0 * filtered_float / observed_float) +// } +// } pub fn get_invalid_filters_string(filters: &Filters, language: Language) -> String { let mut ret_val = format!("{}:", invalid_filters_translation(language));