Skip to main content

ferrix_app/
widgets.rs

1/* widgets.rs
2 *
3 * Copyright 2025-2026 Michail Krasnov <mskrasnov07@ya.ru>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 *
18 * SPDX-License-Identifier: GPL-3.0-or-later
19 */
20
21//! Custom widgets for UI
22
23use iced::{
24    Alignment::Center,
25    Color, Element, Theme, Border,
26    widget::{
27        Column, button, column, container, row, rule, svg, svg::Handle, text, text::IntoFragment,
28        tooltip, tooltip::Position,
29    },
30};
31
32pub mod card;
33pub mod line_charts;
34pub mod table;
35pub mod separated_view;
36
37use crate::{
38    icons::{ABOUT_ICON, ERROR_ICON, EXPORT_ICON, SETTINGS_ICON},
39    messages::{ButtonsMessage, Message},
40    pages::Page,
41};
42
43pub fn icon_tooltip<'a, T>(icon_name: &'a str, tooltip: T) -> container::Container<'a, Message>
44where
45    T: IntoFragment<'a>,
46{
47    let svg_bytes = match icon_name {
48        "about" => ABOUT_ICON,
49        "error" => ERROR_ICON,
50        "export" => EXPORT_ICON,
51        "settings" => SETTINGS_ICON,
52        _ => &[],
53    };
54    let icon = svg(Handle::from_memory(svg_bytes))
55        .style(|theme: &iced::Theme, _| svg::Style {
56            color: Some(theme.palette().text),
57        })
58        .width(16)
59        .height(16);
60
61    container(iced::widget::tooltip(
62        icon,
63        container(text(tooltip).style(|s: &iced::Theme| text::Style {
64            color: Some(if s.extended_palette().is_dark {
65                s.palette().text
66            } else {
67                Color::WHITE
68            }),
69        }))
70        .padding(2)
71        .style(|_| container::Style {
72            background: Some(iced::Background::Color(Color::from_rgba8(0, 0, 0, 0.71))),
73            border: iced::Border {
74                radius: iced::border::Radius::from(2),
75                ..iced::Border::default()
76            },
77            ..Default::default()
78        }),
79        Position::Bottom,
80    ))
81    .width(16)
82    .height(16)
83}
84
85pub fn icon_button<'a>(icon_name: &'a str, tooltip: String) -> button::Button<'a, Message> {
86    let svg_bytes = match icon_name {
87        "about" => ABOUT_ICON,
88        "error" => ERROR_ICON,
89        "export" => EXPORT_ICON,
90        "settings" => SETTINGS_ICON,
91        _ => &[],
92    };
93    let icon = svg(Handle::from_memory(svg_bytes)).style(|theme: &iced::Theme, _| svg::Style {
94        color: Some(theme.palette().text),
95    });
96
97    button(iced::widget::tooltip(
98        icon.width(16).height(16),
99        container(text(tooltip).size(11).style(|s: &iced::Theme| text::Style {
100            color: Some(if s.extended_palette().is_dark {
101                s.palette().text
102            } else {
103                Color::WHITE
104            }),
105        }))
106        .padding(2)
107        .style(|_| container::Style {
108            background: Some(iced::Background::Color(Color::from_rgba8(0, 0, 0, 0.71))),
109            border: iced::Border {
110                radius: iced::border::Radius::from(2),
111                ..iced::Border::default()
112            },
113            ..Default::default()
114        }),
115        Position::Bottom,
116    ))
117    .style(button::subtle)
118    .padding(2)
119}
120
121pub fn sidebar_button<'a>(page: Page, cur_page: Page) -> button::Button<'a, Message> {
122    button(text(page.title_str()))
123        .style(if page != cur_page {
124            button::subtle
125        } else {
126            button::secondary
127        })
128        .on_press(Message::SelectPage(page))
129}
130
131pub fn link_button<'a, P, L>(placeholder: P, link: L) -> tooltip::Tooltip<'a, Message>
132where
133    P: IntoFragment<'a>,
134    L: ToString + IntoFragment<'a> + 'a,
135{
136    tooltip(
137        button(text(placeholder))
138            .style(super::styles::link_button)
139            .padding(0)
140            .on_press(Message::Buttons(ButtonsMessage::LinkButtonPressed(
141                link.to_string(),
142            ))),
143        container(text(link).size(11).style(|s: &iced::Theme| text::Style {
144            color: Some(if s.extended_palette().is_dark {
145                s.palette().text
146            } else {
147                Color::WHITE
148            }),
149        }))
150        .padding(2)
151        .style(|_| container::Style {
152            background: Some(iced::Background::Color(Color::from_rgba8(0, 0, 0, 0.71))),
153            border: iced::Border {
154                radius: iced::border::Radius::from(2),
155                ..iced::Border::default()
156            },
157            ..Default::default()
158        }),
159        Position::Bottom,
160    )
161}
162
163pub fn header<'a, T>(txt: T) -> row::Row<'a, Message>
164where
165    T: IntoFragment<'a> + 'a,
166{
167    row![text(txt).size(16), rule::horizontal(1),]
168        .spacing(5)
169        .align_y(Center)
170}
171
172pub fn header_text<'a>(txt: String) -> Column<'a, Message> {
173    column![text(txt).size(22), rule::horizontal(1)].spacing(2)
174}
175
176pub fn category_header<'a, T>(txt: T) -> text::Text<'a>
177where
178    T: IntoFragment<'a> + 'a,
179{
180    text(txt).size(14).style(|t: &Theme| {
181        let palette = t.palette();
182        let text_color = palette.text.scale_alpha(0.7);
183
184        let mut style = text::Style::default();
185        style.color = Some(text_color);
186
187        style
188    })
189}
190
191pub fn glassy_container<'a, T, C>(header: T, content: C) -> container::Container<'a, Message>
192where
193    T: IntoFragment<'a> + 'a,
194    C: Into<Element<'a, Message>> + 'a,
195{
196    container(column![category_header(header), content.into()].spacing(5))
197        .padding(5)
198        .style(|theme: &iced::Theme| {
199            let is_dark = theme.extended_palette().is_dark;
200            let text_color = theme.palette().text;
201
202            let base_color = match is_dark {
203                true => text_color,
204                false => theme.extended_palette().background.strong.color,
205            };
206            let background_color = base_color.scale_alpha(match is_dark {
207                true => 0.03,
208                false => 0.7,
209            });
210            let border_color = match is_dark {
211                true => base_color,
212                false => iced::Color::BLACK,
213            }.scale_alpha(0.08);
214
215            container::Style::default().background(background_color).border(Border {
216                color: border_color,
217                width: 1.,
218                radius: 5.0.into(),
219            })
220        })
221}