diff --git a/gui/ui/examples/design-system/src/main.rs b/gui/ui/examples/design-system/src/main.rs index 705afdde..bbbc5b59 100644 --- a/gui/ui/examples/design-system/src/main.rs +++ b/gui/ui/examples/design-system/src/main.rs @@ -1,8 +1,8 @@ mod section; -use iced::widget::{button, column, container, radio, row, text, Column, Space}; +use iced::widget::{button, column, container, radio, row, text, Space}; use iced::{executor, Application, Command, Length, Settings, Subscription}; -use liana_ui::{theme, widget::Element}; +use liana_ui::{theme, widget::*}; pub fn main() -> iced::Result { DesignSystem::run(Settings::with_flags(Config {})) @@ -21,6 +21,7 @@ struct DesignSystem { pub enum ThemeType { Light, Dark, + Legacy, } #[derive(Debug, Clone)] @@ -77,6 +78,7 @@ impl Application for DesignSystem { self.theme = match theme { ThemeType::Light => theme::Theme::Light, ThemeType::Dark => theme::Theme::Dark, + ThemeType::Legacy => theme::Theme::Legacy, } } Message::Section(i) => { @@ -118,6 +120,7 @@ impl Application for DesignSystem { Some(match self.theme { theme::Theme::Light => ThemeType::Light, theme::Theme::Dark => ThemeType::Dark, + theme::Theme::Legacy => ThemeType::Legacy, }), Message::ThemeChanged, )) diff --git a/gui/ui/examples/design-system/src/section.rs b/gui/ui/examples/design-system/src/section.rs index 516b6630..930df1ab 100644 --- a/gui/ui/examples/design-system/src/section.rs +++ b/gui/ui/examples/design-system/src/section.rs @@ -3,7 +3,7 @@ use iced::{ widget::{button, column, container, row, Space}, Alignment, Length, }; -use liana_ui::{color, text::*, theme, widget::Element}; +use liana_ui::{color, component::text::*, theme, widget::Element}; use super::{Message, Section}; diff --git a/gui/ui/src/color.rs b/gui/ui/src/color.rs index 69109d73..bfb37d71 100644 --- a/gui/ui/src/color.rs +++ b/gui/ui/src/color.rs @@ -40,3 +40,87 @@ pub const RED: Color = Color::from_rgb( pub const ORANGE: Color = Color::from_rgb(0xFF as f32 / 255.0, 0xa7 as f32 / 255.0, 0x0 as f32 / 255.0); + +pub mod legacy { + use iced::Color; + + pub const BACKGROUND: Color = Color::from_rgb( + 0xF6 as f32 / 255.0, + 0xF7 as f32 / 255.0, + 0xF8 as f32 / 255.0, + ); + + pub const BORDER_GREY: Color = Color::from_rgb( + 0xd0 as f32 / 255.0, + 0xd7 as f32 / 255.0, + 0xde as f32 / 255.0, + ); + + pub const FOREGROUND: Color = Color::WHITE; + + pub const PRIMARY: Color = Color::BLACK; + + pub const SECONDARY: Color = DARK_GREY; + + pub const SUCCESS: Color = Color::from_rgb( + 0x29 as f32 / 255.0, + 0xBC as f32 / 255.0, + 0x97 as f32 / 255.0, + ); + + #[allow(dead_code)] + pub const SUCCESS_LIGHT: Color = Color::from_rgba( + 0x29 as f32 / 255.0, + 0xBC as f32 / 255.0, + 0x97 as f32 / 255.0, + 0.5f32, + ); + + pub const ALERT: Color = Color::from_rgb( + 0xF0 as f32 / 255.0, + 0x43 as f32 / 255.0, + 0x59 as f32 / 255.0, + ); + + pub const ALERT_LIGHT: Color = Color::from_rgba( + 0xF0 as f32 / 255.0, + 0x43 as f32 / 255.0, + 0x59 as f32 / 255.0, + 0.5f32, + ); + + pub const WARNING: Color = + Color::from_rgb(0xFF as f32 / 255.0, 0xa7 as f32 / 255.0, 0x0 as f32 / 255.0); + + pub const WARNING_LIGHT: Color = Color::from_rgba( + 0xFF as f32 / 255.0, + 0xa7 as f32 / 255.0, + 0x0 as f32 / 255.0, + 0.5f32, + ); + + pub const CANCEL: Color = Color::from_rgb( + 0x34 as f32 / 255.0, + 0x37 as f32 / 255.0, + 0x3D as f32 / 255.0, + ); + + pub const INFO: Color = Color::from_rgb( + 0x2A as f32 / 255.0, + 0x98 as f32 / 255.0, + 0xBD as f32 / 255.0, + ); + + pub const INFO_LIGHT: Color = Color::from_rgba( + 0x2A as f32 / 255.0, + 0x98 as f32 / 255.0, + 0xBD as f32 / 255.0, + 0.5f32, + ); + + pub const DARK_GREY: Color = Color::from_rgb( + 0x8c as f32 / 255.0, + 0x97 as f32 / 255.0, + 0xa6 as f32 / 255.0, + ); +} diff --git a/gui/ui/src/component/badge.rs b/gui/ui/src/component/badge.rs new file mode 100644 index 00000000..1d159026 --- /dev/null +++ b/gui/ui/src/component/badge.rs @@ -0,0 +1,101 @@ +use iced::{widget::tooltip, Length}; + +use crate::{component::text::*, icon, theme, widget::*}; + +pub struct Badge { + icon: crate::widget::Text<'static>, + style: theme::Badge, +} + +impl Badge { + pub fn new(icon: crate::widget::Text<'static>) -> Self { + Self { + icon, + style: theme::Badge::Standard, + } + } + pub fn style(self, style: theme::Badge) -> Self { + Self { + icon: self.icon, + style, + } + } +} + +impl<'a, Message: 'a> From for Element<'a, Message> { + fn from(badge: Badge) -> Element<'a, Message> { + Container::new(badge.icon.width(Length::Units(20))) + .width(Length::Units(40)) + .height(Length::Units(40)) + .style(theme::Container::Badge(badge.style)) + .center_x() + .center_y() + .into() + } +} + +pub fn receive() -> Container<'static, T> { + Container::new(icon::receive_icon().width(Length::Units(20))) + .width(Length::Units(40)) + .height(Length::Units(40)) + .style(theme::Container::Badge(theme::Badge::Standard)) + .center_x() + .center_y() +} + +pub fn spend() -> Container<'static, T> { + Container::new(icon::send_icon().width(Length::Units(20))) + .width(Length::Units(40)) + .height(Length::Units(40)) + .style(theme::Container::Badge(theme::Badge::Standard)) + .center_x() + .center_y() +} + +pub fn coin() -> Container<'static, T> { + Container::new(icon::coin_icon().width(Length::Units(20))) + .width(Length::Units(40)) + .height(Length::Units(40)) + .style(theme::Container::Badge(theme::Badge::Standard)) + .center_x() + .center_y() +} + +pub fn unconfirmed<'a, T: 'a>() -> Container<'a, T> { + Container::new( + tooltip::Tooltip::new( + Container::new(text(" Unconfirmed ").small()) + .padding(3) + .style(theme::Container::Pill(theme::Pill::Simple)), + "Do not treat this as a payment until it is confirmed", + tooltip::Position::Top, + ) + .style(theme::Container::Card(theme::Card::Simple)), + ) +} + +pub fn deprecated<'a, T: 'a>() -> Container<'a, T> { + Container::new( + tooltip::Tooltip::new( + Container::new(text(" Deprecated ").small()) + .padding(3) + .style(theme::Container::Pill(theme::Pill::Simple)), + "This spend cannot be included anymore in the blockchain", + tooltip::Position::Top, + ) + .style(theme::Container::Card(theme::Card::Simple)), + ) +} + +pub fn spent<'a, T: 'a>() -> Container<'a, T> { + Container::new( + tooltip::Tooltip::new( + Container::new(text(" Spent ").small()) + .padding(3) + .style(theme::Container::Pill(theme::Pill::Simple)), + "The spend transaction was included in the blockchain", + tooltip::Position::Top, + ) + .style(theme::Container::Card(theme::Card::Simple)), + ) +} diff --git a/gui/ui/src/component/button.rs b/gui/ui/src/component/button.rs new file mode 100644 index 00000000..e6162fe1 --- /dev/null +++ b/gui/ui/src/component/button.rs @@ -0,0 +1,40 @@ +use crate::{theme, widget::*}; +use iced::widget::{button, container, row}; +use iced::{Alignment, Length}; + +use super::text::text; + +pub fn alert<'a, T: 'a>(icon: Option>, t: &'static str) -> Button<'a, T> { + button::Button::new(content(icon, t)).style(theme::Button::Destructive.into()) +} + +pub fn primary<'a, T: 'a>(icon: Option>, t: &'static str) -> Button<'a, T> { + button::Button::new(content(icon, t)).style(theme::Button::Primary.into()) +} + +pub fn transparent<'a, T: 'a>(icon: Option>, t: &'static str) -> Button<'a, T> { + button::Button::new(content(icon, t)).style(theme::Button::Transparent.into()) +} + +pub fn border<'a, T: 'a>(icon: Option>, t: &'static str) -> Button<'a, T> { + button::Button::new(content(icon, t)).style(theme::Button::Secondary.into()) +} + +pub fn transparent_border<'a, T: 'a>(icon: Option>, t: &'static str) -> Button<'a, T> { + button(content(icon, t)).style(theme::Button::TransparentBorder.into()) +} + +fn content<'a, T: 'a>(icon: Option>, t: &'static str) -> Container<'a, T> { + match icon { + None => container(text(t)).width(Length::Fill).center_x().padding(5), + Some(i) => container( + row![i, text(t)] + .spacing(10) + .width(iced::Length::Fill) + .align_items(Alignment::Center), + ) + .width(iced::Length::Fill) + .center_x() + .padding(5), + } +} diff --git a/gui/ui/src/component/card.rs b/gui/ui/src/component/card.rs new file mode 100644 index 00000000..44c9d1e4 --- /dev/null +++ b/gui/ui/src/component/card.rs @@ -0,0 +1,44 @@ +use crate::{color, component::text::text, icon, theme, widget::*}; + +pub fn simple<'a, T: 'a, C: Into>>(content: C) -> Container<'a, T> { + Container::new(content) + .padding(15) + .style(theme::Container::Card(theme::Card::Simple)) +} + +pub fn invalid<'a, T: 'a, C: Into>>(content: C) -> Container<'a, T> { + Container::new(content) + .padding(15) + .style(theme::Container::Card(theme::Card::Invalid)) +} + +/// display an error card with the message and the error in a tooltip. +pub fn warning<'a, T: 'a>(message: String) -> Container<'a, T> { + Container::new( + Row::new() + .spacing(20) + .align_items(iced::Alignment::Center) + .push(icon::warning_octagon_icon().style(color::legacy::WARNING)) + .push(text(message).style(color::legacy::WARNING)), + ) + .padding(15) + .style(theme::Container::Card(theme::Card::Warning)) +} + +/// display an error card with the message and the error in a tooltip. +pub fn error<'a, T: 'a>(message: &'static str, error: String) -> Container<'a, T> { + Container::new( + iced::widget::tooltip::Tooltip::new( + Row::new() + .spacing(20) + .align_items(iced::Alignment::Center) + .push(icon::warning_icon().style(color::legacy::ALERT)) + .push(text(message).style(color::legacy::ALERT)), + error, + iced::widget::tooltip::Position::Bottom, + ) + .style(theme::Container::Card(theme::Card::Error)), + ) + .padding(15) + .style(theme::Container::Card(theme::Card::Error)) +} diff --git a/gui/ui/src/component/collapse.rs b/gui/ui/src/component/collapse.rs new file mode 100644 index 00000000..154ebcaa --- /dev/null +++ b/gui/ui/src/component/collapse.rs @@ -0,0 +1,83 @@ +use crate::widget::*; +use iced::widget::column; +use iced_lazy::{self, Component}; +use std::marker::PhantomData; + +pub struct Collapse<'a, M, H, F, C> { + before: H, + after: F, + content: C, + phantom: PhantomData<&'a M>, +} + +impl<'a, Message, T, H, F, C> Collapse<'a, Message, H, F, C> +where + Message: 'a, + T: Into + Clone + 'a, + H: Fn() -> Button<'a, Event> + 'a, + F: Fn() -> Button<'a, Event> + 'a, + C: Fn() -> Element<'a, T> + 'a, +{ + pub fn new(before: H, after: F, content: C) -> Self { + Collapse { + before, + after, + content, + phantom: PhantomData, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub enum Event { + Internal(T), + Collapse(bool), +} + +impl<'a, Message, T, H, F, C> Component> + for Collapse<'a, Message, H, F, C> +where + T: Into + Clone + 'a, + H: Fn() -> Button<'a, Event>, + F: Fn() -> Button<'a, Event>, + C: Fn() -> Element<'a, T>, +{ + type State = bool; + type Event = Event; + + fn update(&mut self, state: &mut Self::State, event: Event) -> Option { + match event { + Event::Internal(e) => Some(e.into()), + Event::Collapse(s) => { + *state = s; + None + } + } + } + + fn view(&self, state: &Self::State) -> Element { + if *state { + column![ + (self.after)().on_press(Event::Collapse(false)), + (self.content)().map(Event::Internal) + ] + .into() + } else { + column![(self.before)().on_press(Event::Collapse(true))].into() + } + } +} + +impl<'a, Message, T, H: 'a, F: 'a, C: 'a> From> + for Element<'a, Message> +where + Message: 'a, + T: Into + Clone + 'a, + H: Fn() -> Button<'a, Event>, + F: Fn() -> Button<'a, Event>, + C: Fn() -> Element<'a, T>, +{ + fn from(c: Collapse<'a, Message, H, F, C>) -> Self { + iced_lazy::component(c) + } +} diff --git a/gui/ui/src/component/form.rs b/gui/ui/src/component/form.rs new file mode 100644 index 00000000..d5682592 --- /dev/null +++ b/gui/ui/src/component/form.rs @@ -0,0 +1,87 @@ +use iced::{widget::text_input, Length}; + +use crate::{color, component::text::*, theme, util::Collection, widget::*}; + +#[derive(Debug, Clone)] +pub struct Value { + pub value: T, + pub valid: bool, +} + +impl std::default::Default for Value { + fn default() -> Self { + Self { + value: "".to_string(), + valid: true, + } + } +} + +pub struct Form<'a, Message> { + input: text_input::TextInput<'a, Message, iced::Renderer>, + warning: Option<&'a str>, + valid: bool, +} + +impl<'a, Message: 'a> Form<'a, Message> +where + Message: Clone, +{ + /// Creates a new [`Form`]. + /// + /// It expects: + /// - a placeholder + /// - the current value + /// - a function that produces a message when the [`Form`] changes + pub fn new(placeholder: &str, value: &Value, on_change: F) -> Self + where + F: 'static + Fn(String) -> Message, + { + Self { + input: text_input::TextInput::new(placeholder, &value.value, on_change), + warning: None, + valid: value.valid, + } + } + + /// Sets the [`Form`] with a warning message + pub fn warning(mut self, warning: &'a str) -> Self { + self.warning = Some(warning); + self + } + + /// Sets the padding of the [`Form`]. + pub fn padding(mut self, units: u16) -> Self { + self.input = self.input.padding(units); + self + } + + /// Sets the [`Form`] with a text size + pub fn size(mut self, size: u16) -> Self { + self.input = self.input.size(size); + self + } +} + +impl<'a, Message: 'a + Clone> From> for Element<'a, Message> { + fn from(form: Form<'a, Message>) -> Element<'a, Message> { + Container::new( + Column::new() + .push(if !form.valid { + form.input.style(theme::Form::Invalid) + } else { + form.input + }) + .push_maybe(if !form.valid { + form.warning + .map(|message| text(message).style(color::legacy::ALERT).small()) + } else { + None + }) + .width(Length::Fill) + .spacing(5), + ) + .width(Length::Fill) + .into() + } +} diff --git a/gui/ui/src/component/mod.rs b/gui/ui/src/component/mod.rs new file mode 100644 index 00000000..e8ac9f8e --- /dev/null +++ b/gui/ui/src/component/mod.rs @@ -0,0 +1,21 @@ +pub mod badge; +pub mod button; +pub mod card; +pub mod collapse; +pub mod form; +pub mod modal; +pub mod notification; +pub mod text; +pub mod tooltip; + +pub use tooltip::tooltip; + +use iced::Length; + +use crate::{theme, widget::*}; + +pub fn separation<'a, T: 'a>() -> Container<'a, T> { + Container::new(Column::new().push(Text::new(" "))) + .style(theme::Container::Border) + .height(Length::Units(1)) +} diff --git a/gui/ui/src/component/modal.rs b/gui/ui/src/component/modal.rs new file mode 100644 index 00000000..261bf819 --- /dev/null +++ b/gui/ui/src/component/modal.rs @@ -0,0 +1,279 @@ +/// modal widget from https://github.com/iced-rs/iced/blob/master/examples/modal/ +use iced_native::alignment::Alignment; +use iced_native::widget::{self, Tree}; +use iced_native::{ + event, layout, mouse, overlay, renderer, Clipboard, Color, Element, Event, Layout, Length, + Point, Rectangle, Shell, Size, Widget, +}; + +/// A widget that centers a modal element over some base element +pub struct Modal<'a, Message, Renderer> { + base: Element<'a, Message, Renderer>, + modal: Element<'a, Message, Renderer>, + on_blur: Option, +} + +impl<'a, Message, Renderer> Modal<'a, Message, Renderer> { + /// Returns a new [`Modal`] + pub fn new( + base: impl Into>, + modal: impl Into>, + ) -> Self { + Self { + base: base.into(), + modal: modal.into(), + on_blur: None, + } + } + + /// Sets the message that will be produces when the background + /// of the [`Modal`] is pressed + pub fn on_blur(self, on_blur: Option) -> Self { + Self { on_blur, ..self } + } +} + +impl<'a, Message, Renderer> Widget for Modal<'a, Message, Renderer> +where + Renderer: iced_native::Renderer, + Message: Clone, +{ + fn children(&self) -> Vec { + vec![Tree::new(&self.base), Tree::new(&self.modal)] + } + + fn diff(&self, tree: &mut Tree) { + tree.diff_children(&[&self.base, &self.modal]); + } + + fn width(&self) -> Length { + self.base.as_widget().width() + } + + fn height(&self) -> Length { + self.base.as_widget().height() + } + + fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + self.base.as_widget().layout(renderer, limits) + } + + fn on_event( + &mut self, + state: &mut Tree, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + self.base.as_widget_mut().on_event( + &mut state.children[0], + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + ) + } + + fn draw( + &self, + state: &Tree, + renderer: &mut Renderer, + theme: &::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + ) { + self.base.as_widget().draw( + &state.children[0], + renderer, + theme, + style, + layout, + cursor_position, + viewport, + ); + } + + fn overlay<'b>( + &'b mut self, + state: &'b mut Tree, + layout: Layout<'_>, + _renderer: &Renderer, + ) -> Option> { + Some(overlay::Element::new( + layout.position(), + Box::new(Overlay { + content: &mut self.modal, + tree: &mut state.children[1], + size: layout.bounds().size(), + on_blur: self.on_blur.clone(), + }), + )) + } + + fn mouse_interaction( + &self, + state: &Tree, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.base.as_widget().mouse_interaction( + &state.children[0], + layout, + cursor_position, + viewport, + renderer, + ) + } + + fn operate( + &self, + state: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + self.base + .as_widget() + .operate(&mut state.children[0], layout, renderer, operation); + } +} + +struct Overlay<'a, 'b, Message, Renderer> { + content: &'b mut Element<'a, Message, Renderer>, + tree: &'b mut Tree, + size: Size, + on_blur: Option, +} + +impl<'a, 'b, Message, Renderer> overlay::Overlay + for Overlay<'a, 'b, Message, Renderer> +where + Renderer: iced_native::Renderer, + Message: Clone, +{ + fn layout(&self, renderer: &Renderer, _bounds: Size, position: Point) -> layout::Node { + let limits = layout::Limits::new(Size::ZERO, self.size) + .width(Length::Fill) + .height(Length::Fill); + + let mut child = self.content.as_widget().layout(renderer, &limits); + child.align(Alignment::Center, Alignment::Center, limits.max()); + + let mut node = layout::Node::with_children(self.size, vec![child]); + node.move_to(position); + + node + } + + fn on_event( + &mut self, + event: Event, + layout: Layout<'_>, + cursor_position: Point, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) -> event::Status { + let content_bounds = layout.children().next().unwrap().bounds(); + + if let Some(message) = self.on_blur.as_ref() { + if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) = &event { + if !content_bounds.contains(cursor_position) { + shell.publish(message.clone()); + return event::Status::Captured; + } + } + } + + self.content.as_widget_mut().on_event( + self.tree, + event, + layout.children().next().unwrap(), + cursor_position, + renderer, + clipboard, + shell, + ) + } + + fn draw( + &self, + renderer: &mut Renderer, + theme: &Renderer::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: Point, + ) { + renderer.fill_quad( + renderer::Quad { + bounds: layout.bounds(), + border_radius: renderer::BorderRadius::from(0.0), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + Color { + a: 0.80, + ..Color::BLACK + }, + ); + + self.content.as_widget().draw( + self.tree, + renderer, + theme, + style, + layout.children().next().unwrap(), + cursor_position, + &layout.bounds(), + ); + } + + fn operate( + &mut self, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + self.content.as_widget().operate( + self.tree, + layout.children().next().unwrap(), + renderer, + operation, + ); + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor_position: Point, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.content.as_widget().mouse_interaction( + self.tree, + layout.children().next().unwrap(), + cursor_position, + viewport, + renderer, + ) + } +} + +impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> +where + Renderer: 'a + iced_native::Renderer, + Message: 'a + Clone, +{ + fn from(modal: Modal<'a, Message, Renderer>) -> Self { + Element::new(modal) + } +} diff --git a/gui/ui/src/component/notification.rs b/gui/ui/src/component/notification.rs new file mode 100644 index 00000000..212b44e0 --- /dev/null +++ b/gui/ui/src/component/notification.rs @@ -0,0 +1,49 @@ +use crate::{ + component::{collapse, text::*}, + icon, theme, + widget::*, +}; +use iced::{Alignment, Length}; + +pub fn warning<'a, T: 'a + Clone>(message: String, error: String) -> Container<'a, T> { + let message_clone = message.clone(); + Container::new(Container::new(collapse::Collapse::new( + move || { + Button::new( + Row::new() + .push( + Container::new(text(message_clone.to_string()).small().bold()) + .width(Length::Fill), + ) + .push( + Row::new() + .align_items(Alignment::Center) + .spacing(10) + .push(text("Learn more").small().bold()) + .push(icon::collapse_icon()), + ), + ) + .style(theme::Button::Transparent) + }, + move || { + Button::new( + Row::new() + .push( + Container::new(text(message.to_owned()).small().bold()).width(Length::Fill), + ) + .push( + Row::new() + .align_items(Alignment::Center) + .spacing(10) + .push(text("Learn more").small().bold()) + .push(icon::collapsed_icon()), + ), + ) + .style(theme::Button::Transparent) + }, + move || Element::<'a, T>::from(text(error.to_owned()).small()), + ))) + .padding(15) + .style(theme::Container::Card(theme::Card::Warning)) + .width(Length::Fill) +} diff --git a/gui/ui/src/text.rs b/gui/ui/src/component/text.rs similarity index 100% rename from gui/ui/src/text.rs rename to gui/ui/src/component/text.rs diff --git a/gui/ui/src/component/tooltip.rs b/gui/ui/src/component/tooltip.rs new file mode 100644 index 00000000..a0fefda7 --- /dev/null +++ b/gui/ui/src/component/tooltip.rs @@ -0,0 +1,12 @@ +use crate::{icon, theme, widget::*}; + +pub fn tooltip<'a, T: 'a>(help: &'static str) -> Container<'a, T> { + Container::new( + iced::widget::tooltip::Tooltip::new( + icon::tooltip_icon(), + help, + iced::widget::tooltip::Position::Right, + ) + .style(theme::Container::Card(theme::Card::Simple)), + ) +} diff --git a/gui/ui/src/icon.rs b/gui/ui/src/icon.rs new file mode 100644 index 00000000..ee23330f --- /dev/null +++ b/gui/ui/src/icon.rs @@ -0,0 +1,233 @@ +use crate::widget::*; +use iced::{alignment, Font, Length}; + +const ICONS: Font = Font::External { + name: "Icons", + bytes: include_bytes!("../../static/icons/bootstrap-icons.ttf"), +}; + +fn icon(unicode: char) -> Text<'static> { + Text::new(unicode.to_string()) + .font(ICONS) + .width(Length::Units(20)) + .horizontal_alignment(alignment::Horizontal::Center) + .size(20) +} + +pub fn arrow_down() -> Text<'static> { + icon('\u{F128}') +} + +pub fn chevron_right() -> Text<'static> { + icon('\u{F285}') +} + +pub fn recovery_icon() -> Text<'static> { + icon('\u{F467}') +} + +pub fn plug_icon() -> Text<'static> { + icon('\u{F4F6}') +} + +pub fn reload_icon() -> Text<'static> { + icon('\u{F130}') +} + +pub fn import_icon() -> Text<'static> { + icon('\u{F30A}') +} + +pub fn wallet_icon() -> Text<'static> { + icon('\u{F615}') +} + +pub fn hourglass_icon() -> Text<'static> { + icon('\u{F41F}') +} + +pub fn hourglass_done_icon() -> Text<'static> { + icon('\u{F41E}') +} + +pub fn vault_icon() -> Text<'static> { + icon('\u{F65A}') +} + +pub fn bitcoin_icon() -> Text<'static> { + icon('\u{F635}') +} + +pub fn history_icon() -> Text<'static> { + icon('\u{F292}') +} + +pub fn home_icon() -> Text<'static> { + icon('\u{F3FC}') +} + +pub fn unlock_icon() -> Text<'static> { + icon('\u{F600}') +} + +pub fn warning_octagon_icon() -> Text<'static> { + icon('\u{F337}') +} + +pub fn send_icon() -> Text<'static> { + icon('\u{F144}') +} + +pub fn connect_device_icon() -> Text<'static> { + icon('\u{F348}') +} + +pub fn connected_device_icon() -> Text<'static> { + icon('\u{F350}') +} + +pub fn receive_icon() -> Text<'static> { + icon('\u{F123}') +} + +pub fn calendar_icon() -> Text<'static> { + icon('\u{F1E8}') +} + +pub fn turnback_icon() -> Text<'static> { + icon('\u{F131}') +} + +pub fn vaults_icon() -> Text<'static> { + icon('\u{F1C7}') +} + +pub fn coin_icon() -> Text<'static> { + icon('\u{F567}') +} + +pub fn settings_icon() -> Text<'static> { + icon('\u{F3E5}') +} + +pub fn block_icon() -> Text<'static> { + icon('\u{F1C8}') +} + +pub fn square_icon() -> Text<'static> { + icon('\u{F584}') +} + +pub fn square_check_icon() -> Text<'static> { + icon('\u{F26D}') +} + +pub fn circle_check_icon() -> Text<'static> { + icon('\u{F26B}') +} + +pub fn circle_cross_icon() -> Text<'static> { + icon('\u{F623}') +} + +pub fn network_icon() -> Text<'static> { + icon('\u{F40D}') +} + +pub fn dot_icon() -> Text<'static> { + icon('\u{F287}') +} + +pub fn clipboard_icon() -> Text<'static> { + icon('\u{F3C2}') +} + +pub fn shield_icon() -> Text<'static> { + icon('\u{F53F}') +} + +pub fn shield_notif_icon() -> Text<'static> { + icon('\u{F530}') +} + +pub fn shield_check_icon() -> Text<'static> { + icon('\u{F52F}') +} + +pub fn person_check_icon() -> Text<'static> { + icon('\u{F4D6}') +} + +pub fn person_icon() -> Text<'static> { + icon('\u{F4DA}') +} + +pub fn tooltip_icon() -> Text<'static> { + icon('\u{F431}') +} + +pub fn plus_icon() -> Text<'static> { + icon('\u{F4FE}') +} + +pub fn warning_icon() -> Text<'static> { + icon('\u{F33B}') +} + +pub fn chip_icon() -> Text<'static> { + icon('\u{F2D6}') +} + +pub fn trash_icon() -> Text<'static> { + icon('\u{F5DE}') +} + +pub fn key_icon() -> Text<'static> { + icon('\u{F44F}') +} + +pub fn cross_icon() -> Text<'static> { + icon('\u{F62A}') +} + +pub fn pencil_icon() -> Text<'static> { + icon('\u{F4CB}') +} + +#[allow(dead_code)] +pub fn stakeholder_icon() -> Text<'static> { + icon('\u{F4AE}') +} + +#[allow(dead_code)] +pub fn manager_icon() -> Text<'static> { + icon('\u{F4B4}') +} + +pub fn done_icon() -> Text<'static> { + icon('\u{F26B}') +} + +pub fn todo_icon() -> Text<'static> { + icon('\u{F28A}') +} + +pub fn collapse_icon() -> Text<'static> { + icon('\u{F284}') +} + +pub fn collapsed_icon() -> Text<'static> { + icon('\u{F282}') +} + +pub fn down_icon() -> Text<'static> { + icon('\u{F279}') +} + +pub fn up_icon() -> Text<'static> { + icon('\u{F27C}') +} + +pub fn people_icon() -> Text<'static> { + icon('\u{F4CF}') +} diff --git a/gui/ui/src/lib.rs b/gui/ui/src/lib.rs index 8f1ec600..3554c8a4 100644 --- a/gui/ui/src/lib.rs +++ b/gui/ui/src/lib.rs @@ -1,7 +1,9 @@ pub mod color; +pub mod component; pub mod font; -pub mod text; +pub mod icon; pub mod theme; +pub mod util; pub mod widget { #![allow(dead_code)] @@ -10,5 +12,9 @@ pub mod widget { pub type Renderer = iced::Renderer; pub type Element<'a, Message> = iced::Element<'a, Message, Renderer>; pub type Container<'a, Message> = iced::widget::Container<'a, Message, Renderer>; + pub type Column<'a, Message> = iced::widget::Column<'a, Message, Renderer>; + pub type Row<'a, Message> = iced::widget::Row<'a, Message, Renderer>; pub type Button<'a, Message> = iced::widget::Button<'a, Message, Renderer>; + pub type Text<'a> = iced::widget::Text<'a, Renderer>; + pub type Tooltip<'a> = iced::widget::Tooltip<'a, Renderer>; } diff --git a/gui/ui/src/theme.rs b/gui/ui/src/theme.rs index 1f82b477..82b7f7c2 100644 --- a/gui/ui/src/theme.rs +++ b/gui/ui/src/theme.rs @@ -1,6 +1,6 @@ use iced::{ application, - widget::{button, container, radio, text}, + widget::{button, container, radio, text, text_input}, }; use super::color; @@ -10,6 +10,7 @@ pub enum Theme { #[default] Dark, Light, + Legacy, } impl application::StyleSheet for Theme { @@ -25,6 +26,10 @@ impl application::StyleSheet for Theme { background_color: color::LIGHT_BLACK, text_color: color::LIGHT_GREY, }, + Theme::Legacy => application::Appearance { + background_color: color::legacy::BACKGROUND, + text_color: color::BLACK, + }, } } } @@ -60,6 +65,9 @@ pub enum Container { Background, Foreground, Border, + Card(Card), + Badge(Badge), + Pill(Pill), Custom(iced::Color), } @@ -86,6 +94,9 @@ impl container::StyleSheet for Theme { border_color: color::LIGHT_BLACK.into(), ..container::Appearance::default() }, + Container::Card(c) => c.appearance(self), + Container::Badge(c) => c.appearance(self), + Container::Pill(c) => c.appearance(self), Container::Custom(c) => container::Appearance { background: (*c).into(), ..container::Appearance::default() @@ -110,11 +121,204 @@ impl container::StyleSheet for Theme { border_color: color::LIGHT_GREY.into(), ..container::Appearance::default() }, + Container::Card(c) => c.appearance(self), + Container::Badge(c) => c.appearance(self), + Container::Pill(c) => c.appearance(self), Container::Custom(c) => container::Appearance { background: (*c).into(), ..container::Appearance::default() }, }, + Theme::Legacy => match style { + Container::Transparent => container::Appearance { + background: iced::Color::TRANSPARENT.into(), + ..container::Appearance::default() + }, + Container::Background => container::Appearance { + background: color::legacy::BACKGROUND.into(), + ..container::Appearance::default() + }, + Container::Foreground => container::Appearance { + background: color::legacy::FOREGROUND.into(), + ..container::Appearance::default() + }, + Container::Border => container::Appearance { + background: iced::Color::TRANSPARENT.into(), + border_width: 1.0, + border_color: color::legacy::BORDER_GREY.into(), + ..container::Appearance::default() + }, + Container::Card(c) => c.appearance(self), + Container::Badge(c) => c.appearance(self), + Container::Pill(c) => c.appearance(self), + Container::Custom(c) => container::Appearance { + background: (*c).into(), + ..container::Appearance::default() + }, + }, + } + } +} + +#[derive(Debug, Copy, Clone, Default)] +pub enum Card { + #[default] + Simple, + Invalid, + Warning, + Error, +} + +impl Card { + fn appearance(&self, theme: &Theme) -> iced::widget::container::Appearance { + match theme { + Theme::Light => match self { + Card::Simple => container::Appearance { + background: color::GREY.into(), + ..container::Appearance::default() + }, + Card::Invalid => container::Appearance { + background: color::GREY.into(), + text_color: color::BLACK.into(), + border_width: 1.0, + border_color: color::RED, + ..container::Appearance::default() + }, + Card::Error => container::Appearance { + background: color::GREY.into(), + text_color: color::RED.into(), + border_width: 1.0, + border_color: color::RED, + ..container::Appearance::default() + }, + Card::Warning => container::Appearance { + background: color::ORANGE.into(), + text_color: color::GREY.into(), + ..container::Appearance::default() + }, + }, + Theme::Dark => match self { + Card::Simple => container::Appearance { + background: color::LIGHT_BLACK.into(), + ..container::Appearance::default() + }, + Card::Invalid => container::Appearance { + background: color::LIGHT_BLACK.into(), + text_color: color::BLACK.into(), + border_width: 1.0, + border_color: color::RED, + ..container::Appearance::default() + }, + Card::Error => container::Appearance { + background: color::LIGHT_BLACK.into(), + text_color: color::RED.into(), + border_width: 1.0, + border_color: color::RED, + ..container::Appearance::default() + }, + Card::Warning => container::Appearance { + background: color::ORANGE.into(), + text_color: color::GREY.into(), + ..container::Appearance::default() + }, + }, + Theme::Legacy => match self { + Card::Simple => container::Appearance { + background: color::legacy::FOREGROUND.into(), + border_radius: 10.0, + border_color: color::legacy::BORDER_GREY, + border_width: 1.0, + ..container::Appearance::default() + }, + Card::Invalid => container::Appearance { + background: color::legacy::FOREGROUND.into(), + text_color: iced::Color::BLACK.into(), + border_width: 1.0, + border_radius: 10.0, + border_color: color::legacy::ALERT, + ..container::Appearance::default() + }, + Card::Error => container::Appearance { + background: color::legacy::FOREGROUND.into(), + text_color: color::legacy::ALERT.into(), + border_width: 1.0, + border_radius: 10.0, + border_color: color::legacy::ALERT, + ..container::Appearance::default() + }, + Card::Warning => container::Appearance { + border_radius: 0.0, + text_color: iced::Color::BLACK.into(), + background: color::legacy::WARNING.into(), + border_color: color::legacy::WARNING, + ..container::Appearance::default() + }, + }, + } + } +} + +#[derive(Debug, Copy, Clone, Default)] +pub enum Badge { + #[default] + Standard, + Bitcoin, +} + +impl Badge { + fn appearance(&self, _theme: &Theme) -> iced::widget::container::Appearance { + match self { + Self::Standard => container::Appearance { + border_radius: 40.0, + background: color::legacy::BACKGROUND.into(), + ..container::Appearance::default() + }, + Self::Bitcoin => container::Appearance { + border_radius: 40.0, + background: color::legacy::WARNING.into(), + text_color: iced::Color::WHITE.into(), + ..container::Appearance::default() + }, + } + } +} + +#[derive(Debug, Copy, Clone, Default)] +pub enum Pill { + #[default] + Simple, + InversePrimary, + Primary, + Success, +} + +impl Pill { + fn appearance(&self, _theme: &Theme) -> iced::widget::container::Appearance { + match self { + Self::Primary => container::Appearance { + background: color::legacy::PRIMARY.into(), + border_radius: 10.0, + text_color: iced::Color::WHITE.into(), + ..container::Appearance::default() + }, + Self::InversePrimary => container::Appearance { + background: color::legacy::FOREGROUND.into(), + border_radius: 10.0, + text_color: color::legacy::PRIMARY.into(), + ..container::Appearance::default() + }, + Self::Success => container::Appearance { + background: color::legacy::SUCCESS.into(), + border_radius: 10.0, + text_color: iced::Color::WHITE.into(), + ..container::Appearance::default() + }, + Self::Simple => container::Appearance { + background: color::legacy::BACKGROUND.into(), + border_radius: 10.0, + text_color: iced::Color::BLACK.into(), + ..container::Appearance::default() + }, } } } @@ -151,6 +355,7 @@ pub enum Button { Secondary, Destructive, Transparent, + TransparentBorder, } impl button::StyleSheet for Theme { @@ -183,7 +388,7 @@ impl button::StyleSheet for Theme { border_color: iced::Color::TRANSPARENT, text_color: color::LIGHT_GREY, }, - Button::Transparent => button::Appearance { + Button::Transparent | Button::TransparentBorder => button::Appearance { shadow_offset: iced::Vector::default(), background: iced::Color::TRANSPARENT.into(), border_radius: 10.0, @@ -217,7 +422,7 @@ impl button::StyleSheet for Theme { border_color: iced::Color::TRANSPARENT, text_color: color::LIGHT_BLACK, }, - Button::Transparent => button::Appearance { + Button::Transparent | Button::TransparentBorder => button::Appearance { shadow_offset: iced::Vector::default(), background: iced::Color::TRANSPARENT.into(), border_radius: 10.0, @@ -226,6 +431,40 @@ impl button::StyleSheet for Theme { text_color: color::LIGHT_GREY, }, }, + Theme::Legacy => match style { + Button::Primary => button::Appearance { + shadow_offset: iced::Vector::default(), + background: color::legacy::PRIMARY.into(), + border_radius: 10.0, + border_width: 0.0, + border_color: iced::Color::TRANSPARENT, + text_color: color::legacy::FOREGROUND, + }, + Button::Destructive => button::Appearance { + shadow_offset: iced::Vector::default(), + background: color::legacy::FOREGROUND.into(), + border_radius: 10.0, + border_width: 0.0, + border_color: color::legacy::ALERT, + text_color: color::legacy::ALERT, + }, + Button::Transparent | Button::TransparentBorder => button::Appearance { + shadow_offset: iced::Vector::default(), + background: iced::Color::TRANSPARENT.into(), + border_radius: 10.0, + border_width: 0.0, + border_color: iced::Color::TRANSPARENT, + text_color: iced::Color::BLACK, + }, + Button::Secondary => button::Appearance { + shadow_offset: iced::Vector::default(), + background: iced::Color::TRANSPARENT.into(), + border_radius: 10.0, + border_width: 1.2, + border_color: color::legacy::BORDER_GREY, + text_color: iced::Color::BLACK, + }, + }, } } @@ -264,6 +503,14 @@ impl button::StyleSheet for Theme { border_color: iced::Color::TRANSPARENT, text_color: color::LIGHT_GREY, }, + Button::TransparentBorder => button::Appearance { + shadow_offset: iced::Vector::default(), + background: color::DARK_GREY.into(), + border_radius: 10.0, + border_width: 1.0, + border_color: color::LIGHT_BLACK, + text_color: color::LIGHT_GREY, + }, }, Theme::Dark => match style { Button::Primary => button::Appearance { @@ -298,7 +545,102 @@ impl button::StyleSheet for Theme { border_color: iced::Color::TRANSPARENT, text_color: color::LIGHT_GREY, }, + Button::TransparentBorder => button::Appearance { + shadow_offset: iced::Vector::default(), + background: color::DARK_GREY.into(), + border_radius: 10.0, + border_width: 1.0, + border_color: color::LIGHT_GREY, + text_color: color::LIGHT_GREY, + }, + }, + Theme::Legacy => match style { + Button::Primary => button::Appearance { + shadow_offset: iced::Vector::default(), + background: color::legacy::PRIMARY.into(), + border_radius: 10.0, + border_width: 0.0, + border_color: iced::Color::TRANSPARENT, + text_color: color::legacy::FOREGROUND, + }, + Button::Destructive => button::Appearance { + shadow_offset: iced::Vector::default(), + background: color::legacy::FOREGROUND.into(), + border_radius: 10.0, + border_width: 0.0, + border_color: color::legacy::ALERT, + text_color: color::legacy::ALERT, + }, + Button::Transparent => button::Appearance { + shadow_offset: iced::Vector::default(), + background: iced::Color::TRANSPARENT.into(), + border_radius: 10.0, + border_width: 0.0, + border_color: iced::Color::TRANSPARENT, + text_color: iced::Color::BLACK, + }, + Button::TransparentBorder => button::Appearance { + shadow_offset: iced::Vector::default(), + background: iced::Color::TRANSPARENT.into(), + border_radius: 10.0, + border_width: 1.0, + border_color: iced::Color::BLACK, + text_color: iced::Color::BLACK, + }, + Button::Secondary => button::Appearance { + shadow_offset: iced::Vector::default(), + background: iced::Color::TRANSPARENT.into(), + border_radius: 10.0, + border_width: 1.0, + border_color: iced::Color::BLACK, + text_color: iced::Color::BLACK, + }, }, } } } + +#[derive(Debug, Copy, Clone, Default)] +pub enum Form { + #[default] + Simple, + Invalid, +} + +impl text_input::StyleSheet for Theme { + type Style = Form; + fn active(&self, style: &Self::Style) -> text_input::Appearance { + match style { + Form::Simple => text_input::Appearance { + background: iced::Background::Color(color::legacy::FOREGROUND), + border_radius: 5.0, + border_width: 1.0, + border_color: color::legacy::DARK_GREY, + }, + Form::Invalid => text_input::Appearance { + background: iced::Background::Color(color::legacy::FOREGROUND), + border_radius: 5.0, + border_width: 1.0, + border_color: color::legacy::ALERT, + }, + } + } + + fn focused(&self, style: &Self::Style) -> text_input::Appearance { + text_input::Appearance { + ..self.active(style) + } + } + + fn placeholder_color(&self, _style: &Self::Style) -> iced::Color { + iced::Color::from_rgb(0.7, 0.7, 0.7) + } + + fn value_color(&self, _style: &Self::Style) -> iced::Color { + iced::Color::from_rgb(0.3, 0.3, 0.3) + } + + fn selection_color(&self, _style: &Self::Style) -> iced::Color { + iced::Color::from_rgb(0.8, 0.8, 1.0) + } +} diff --git a/gui/ui/src/util.rs b/gui/ui/src/util.rs new file mode 100644 index 00000000..2553c2c9 --- /dev/null +++ b/gui/ui/src/util.rs @@ -0,0 +1,25 @@ +/// from hecjr idea on Discord +use crate::widget::*; + +pub trait Collection<'a, Message>: Sized { + fn push(self, element: impl Into>) -> Self; + + fn push_maybe(self, element: Option>>) -> Self { + match element { + Some(element) => self.push(element), + None => self, + } + } +} + +impl<'a, Message> Collection<'a, Message> for Column<'a, Message> { + fn push(self, element: impl Into>) -> Self { + Self::push(self, element) + } +} + +impl<'a, Message> Collection<'a, Message> for Row<'a, Message> { + fn push(self, element: impl Into>) -> Self { + Self::push(self, element) + } +}