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,
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;
35
36use crate::{
37    icons::{ABOUT_ICON, ERROR_ICON, EXPORT_ICON, SETTINGS_ICON},
38    messages::{ButtonsMessage, Message},
39    pages::Page,
40};
41
42pub fn icon_tooltip<'a, T>(icon_name: &'a str, tooltip: T) -> container::Container<'a, Message>
43where
44    T: IntoFragment<'a>,
45{
46    let svg_bytes = match icon_name {
47        "about" => ABOUT_ICON,
48        "error" => ERROR_ICON,
49        "export" => EXPORT_ICON,
50        "settings" => SETTINGS_ICON,
51        _ => &[],
52    };
53    let icon = svg(Handle::from_memory(svg_bytes))
54        .style(|theme: &iced::Theme, _| svg::Style {
55            color: Some(theme.palette().text),
56        })
57        .width(16)
58        .height(16);
59
60    container(iced::widget::tooltip(
61        icon,
62        container(text(tooltip).style(|s: &iced::Theme| text::Style {
63            color: Some(if s.extended_palette().is_dark {
64                s.palette().text
65            } else {
66                Color::WHITE
67            }),
68        }))
69        .padding(2)
70        .style(|_| container::Style {
71            background: Some(iced::Background::Color(Color::from_rgba8(0, 0, 0, 0.71))),
72            border: iced::Border {
73                radius: iced::border::Radius::from(2),
74                ..iced::Border::default()
75            },
76            ..Default::default()
77        }),
78        Position::Bottom,
79    ))
80    .width(16)
81    .height(16)
82}
83
84pub fn icon_button<'a>(icon_name: &'a str, tooltip: String) -> button::Button<'a, Message> {
85    let svg_bytes = match icon_name {
86        "about" => ABOUT_ICON,
87        "error" => ERROR_ICON,
88        "export" => EXPORT_ICON,
89        "settings" => SETTINGS_ICON,
90        _ => &[],
91    };
92    let icon = svg(Handle::from_memory(svg_bytes)).style(|theme: &iced::Theme, _| svg::Style {
93        color: Some(theme.palette().text),
94    });
95
96    button(iced::widget::tooltip(
97        icon.width(16).height(16),
98        container(text(tooltip).size(11).style(|s: &iced::Theme| text::Style {
99            color: Some(if s.extended_palette().is_dark {
100                s.palette().text
101            } else {
102                Color::WHITE
103            }),
104        }))
105        .padding(2)
106        .style(|_| container::Style {
107            background: Some(iced::Background::Color(Color::from_rgba8(0, 0, 0, 0.71))),
108            border: iced::Border {
109                radius: iced::border::Radius::from(2),
110                ..iced::Border::default()
111            },
112            ..Default::default()
113        }),
114        Position::Bottom,
115    ))
116    .style(button::subtle)
117    .padding(2)
118}
119
120pub fn sidebar_button<'a>(page: Page, cur_page: Page) -> button::Button<'a, Message> {
121    button(text(page.title_str()))
122        .style(if page != cur_page {
123            button::subtle
124        } else {
125            button::secondary
126        })
127        .on_press(Message::SelectPage(page))
128}
129
130pub fn link_button<'a, P, L>(placeholder: P, link: L) -> tooltip::Tooltip<'a, Message>
131where
132    P: IntoFragment<'a>,
133    L: ToString + IntoFragment<'a> + 'a,
134{
135    tooltip(
136        button(text(placeholder))
137            .style(super::styles::link_button)
138            .padding(0)
139            .on_press(Message::Buttons(ButtonsMessage::LinkButtonPressed(
140                link.to_string(),
141            ))),
142        container(text(link).size(11).style(|s: &iced::Theme| text::Style {
143            color: Some(if s.extended_palette().is_dark {
144                s.palette().text
145            } else {
146                Color::WHITE
147            }),
148        }))
149        .padding(2)
150        .style(|_| container::Style {
151            background: Some(iced::Background::Color(Color::from_rgba8(0, 0, 0, 0.71))),
152            border: iced::Border {
153                radius: iced::border::Radius::from(2),
154                ..iced::Border::default()
155            },
156            ..Default::default()
157        }),
158        Position::Bottom,
159    )
160}
161
162pub fn header<'a, T>(txt: T) -> row::Row<'a, Message>
163where
164    T: IntoFragment<'a> + 'a,
165{
166    row![text(txt).size(16), rule::horizontal(1),]
167        .spacing(5)
168        .align_y(Center)
169}
170
171pub fn header_text<'a>(txt: String) -> Column<'a, Message> {
172    column![text(txt).size(22), rule::horizontal(1)].spacing(2)
173}
174
175pub fn category_header<'a, T>(txt: T) -> text::Text<'a>
176where
177    T: IntoFragment<'a> + 'a,
178{
179    text(txt).size(14).style(|t: &Theme| {
180        let palette = t.palette();
181        let text_color = palette.text.scale_alpha(0.7);
182
183        let mut style = text::Style::default();
184        style.color = Some(text_color);
185
186        style
187    })
188}
189
190pub fn glassy_container<'a, T, C>(header: T, content: C) -> container::Container<'a, Message>
191where
192    T: IntoFragment<'a> + 'a,
193    C: Into<Element<'a, Message>> + 'a,
194{
195    container(column![category_header(header), content.into()].spacing(5))
196        .padding(5)
197        .style(container::rounded_box)
198}