diff --git a/src/chart/types/donut_chart.rs b/src/chart/types/donut_chart.rs new file mode 100644 index 000000000..e54554132 --- /dev/null +++ b/src/chart/types/donut_chart.rs @@ -0,0 +1,143 @@ +use crate::gui::styles::donut::Catalog; +use crate::gui::styles::style_constants::FONT_SIZE_BODY; +use iced::alignment::{Horizontal, Vertical}; +use iced::widget::canvas::path::Arc; +use iced::widget::canvas::{Frame, Text}; +use iced::widget::text::Shaping; +use iced::widget::{canvas, Canvas}; +use iced::{mouse, Font, Point, Radians, Renderer}; +use std::f32::consts; + +pub struct DonutChart { + percentage: f32, + label: String, + font: Font, +} + +impl DonutChart { + pub fn new(percentage: f32, label: impl Into, font: Font) -> Self { + let label = label.into(); + Self { + percentage, + label, + font, + } + } +} + +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; + + let start_angle1 = Radians(-consts::FRAC_PI_2); + let end_angle1 = Radians(consts::TAU) * self.percentage / 100.0 + start_angle1; + + let style = ::style(theme, &::default()); + + // let circle_at_angle = |angle: Radians| { + // canvas::Path::circle( + // Point::new( + // center.x + fixed_radius * angle.0.cos(), + // center.y + fixed_radius * angle.0.sin(), + // ), + // inner_ball_radius, + // ) + // }; + + let circle = canvas::Path::circle(center, radius); + + let incoming = canvas::Path::new(|builder| { + builder.arc(Arc { + center, + radius, + start_angle: start_angle1, + end_angle: end_angle1, + }); + builder.line_to(center); + builder.close(); + }); + + let outgoing = canvas::Path::new(|builder| { + builder.arc(Arc { + center, + radius, + start_angle: end_angle1, + end_angle: start_angle1 + Radians(consts::PI * 2.0), + }); + builder.line_to(center); + builder.close(); + }); + + let inner_circle = canvas::Path::circle(center, radius - 6.0); + + frame.fill_text(Text { + content: if self.label.is_empty() { + format!("{:.2}%", self.percentage) + } else { + self.label.clone() + }, + position: center, + vertical_alignment: Vertical::Center, + horizontal_alignment: Horizontal::Center, + color: style.text_color, + size: FONT_SIZE_BODY.into(), + font: self.font, + ..Default::default() + }); + frame.fill(&circle, style.background); + frame.fill(&incoming, style.incoming); + frame.fill(&outgoing, style.outgoing); + frame.fill(&inner_circle, style.rail); + + vec![frame.into_geometry()] + } +} + +/// Creates a radial progress bar widget. +/// +/// This function returns a `Canvas` widget that displays a radial progress bar +/// with a specified percentage and content. If the content is empty, the +/// percentage will be displayed by default. +/// +/// # Example +/// +/// ```rust +/// use atoms::widgets::radial_progress_bar; +/// +/// let progress = radial_progress_bar(0.75, "75% Complete"); +/// let default_progress = radial_progress_bar(0.75, ""); +/// ``` +pub fn donut_chart( + percentage: f32, + label: impl Into, + font: Font, +) -> Canvas { + let radius = 95.0; + iced::widget::canvas(DonutChart::new(percentage, label, font)) + .width(radius) + .height(radius) +} + +/// The status of a [`ProgressBar`]. +#[derive(Debug, Clone, Copy)] +pub enum Status { + /// The progress bar is idle. + Idle, + /// The progress bar is currently progressing. + Progressing, + /// The progress bar has finished. + Finished, + /// The progress bar has failed. + Failed, +} 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/overview_page.rs b/src/gui/pages/overview_page.rs index b4324ad41..aa5a57782 100644 --- a/src/gui/pages/overview_page.rs +++ b/src/gui/pages/overview_page.rs @@ -7,12 +7,13 @@ 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, + button, horizontal_space, lazy, vertical_space, Button, Canvas, 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, DonutChart}; use crate::countries::country_utils::get_flag_tooltip; use crate::countries::flags_pictures::FLAGS_WIDTH_BIG; use crate::gui::components::tab::get_pages_tabs; @@ -34,9 +35,10 @@ 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, + filtered_bytes_translation, filtered_packets_translation, ip_version_translation, + network_adapter_translation, no_addresses_translation, none_translation, of_total_translation, + packets_chart_translation, protocol_translation, some_observed_translation, + traffic_rate_translation, waiting_translation, }; use crate::translations::translations_2::{ data_representation_translation, dropped_packets_translation, host_translation, @@ -464,7 +466,7 @@ fn lazy_col_info<'a>( let col_data_representation = col_data_representation(language, font, sniffer.traffic_chart.chart_type); - let col_bytes_packets = col_bytes_packets( + let donut_charts = donut_charts( language, dropped, total, @@ -493,7 +495,7 @@ fn lazy_col_info<'a>( .push(Rule::horizontal(15)) .push( Scrollable::with_direction( - col_bytes_packets, + donut_charts, Direction::Vertical(ScrollbarType::properties()), ) .height(Length::Fill) @@ -603,7 +605,7 @@ fn col_data_representation<'a>( ret_val } -fn col_bytes_packets<'a>( +fn donut_charts<'a>( language: Language, dropped: u32, total: u128, @@ -612,57 +614,75 @@ fn col_bytes_packets<'a>( 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 filtered_bytes = sniffer.runtime_data.tot_out_bytes + sniffer.runtime_data.tot_in_bytes; let all_bytes = sniffer.runtime_data.all_bytes; - let filters = &sniffer.filters; - - let dropped_val = if dropped > 0 { - format!( - "{} {}", - dropped, - of_total_translation(language, &get_percentage_string(total, u128::from(dropped))) - ) - } 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)) - ) - }; + // let filters = &sniffer.filters; + // + // let dropped_val = if dropped > 0 { + // format!( + // "{} {}", + // dropped, + // of_total_translation(language, &get_percentage_string(total, u128::from(dropped))) + // ) + // } 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)) + // ) + // }; + // + // Column::new() + // .spacing(10) + // .push(get_active_filters_col( + // filters, + // language, + // font, + // font_headers, + // false, + // )) + // .push(TextType::highlighted_subtitle_with_desc( + // filtered_bytes_translation(language), + // &bytes_value, + // font, + // )) + // .push(TextType::highlighted_subtitle_with_desc( + // filtered_packets_translation(language), + // &format!( + // "{} {}", + // filtered, + // of_total_translation(language, &get_percentage_string(total, filtered)) + // ), + // font, + // )) + // .push(TextType::highlighted_subtitle_with_desc( + // dropped_packets_translation(language), + // &dropped_val, + // font, + // )) + + let radius = 100; + let donuts_row = Row::new() + .padding(Padding::ZERO.top(10)) + .spacing(20) + .push(donut_chart( + 37.0, + ByteMultiple::formatted_string(all_bytes), + font, + )) + .push(donut_chart(37.0, "IPv", font)) + .push(donut_chart(37.0, "proto", font)); Column::new() + .width(Length::Fill) .spacing(10) - .push(get_active_filters_col( - filters, - language, - font, - font_headers, - false, - )) - .push(TextType::highlighted_subtitle_with_desc( - filtered_bytes_translation(language), - &bytes_value, - font, - )) - .push(TextType::highlighted_subtitle_with_desc( - filtered_packets_translation(language), - &format!( - "{} {}", - filtered, - of_total_translation(language, &get_percentage_string(total, filtered)) - ), - font, - )) - .push(TextType::highlighted_subtitle_with_desc( - dropped_packets_translation(language), - &dropped_val, - font, - )) + .align_x(Alignment::Center) + .push(donuts_row) } const MIN_BARS_LENGTH: f32 = 10.0; diff --git a/src/gui/styles/donut.rs b/src/gui/styles/donut.rs new file mode 100644 index 000000000..1794d1c55 --- /dev/null +++ b/src/gui/styles/donut.rs @@ -0,0 +1,56 @@ +use crate::chart::types::donut_chart::Status; +use crate::gui::styles::button::ButtonType; +use crate::gui::styles::types::style_type::StyleType; +use iced::Color; + +#[derive(Default)] +pub enum DonutType { + #[default] + Standard, +} + +impl DonutType { + fn active(&self, style: &StyleType) -> Style { + let colors = style.get_palette(); + let ext = style.get_extension(); + Style { + background: Color::TRANSPARENT, + incoming: colors.secondary, + outgoing: colors.outgoing, + rail: colors.primary, + text_color: colors.text_body, + } + } +} + +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) incoming: Color, + pub(crate) outgoing: Color, + pub(crate) rail: Color, + pub(crate) text_color: Color, +} + +/// The theme catalog of a [`ProgressBar`]. +pub trait Catalog: Sized { + /// The item class of the [`Catalog`]. + type Class<'a>; + + /// The default class produced by the [`Catalog`]. + fn default<'a>() -> Self::Class<'a>; + + /// The [`Style`] of a class with the given status. + 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;