From 99782d770f7f23fc4e48f367172af1500625a450 Mon Sep 17 00:00:00 2001 From: sonodima Date: Tue, 3 Sep 2024 20:38:58 +0200 Subject: [PATCH] added custom theme --- blurthing/src/application.rs | 286 +++++++++++++--------------- blurthing/src/main.rs | 2 + blurthing/src/styles/application.rs | 14 ++ blurthing/src/styles/button.rs | 95 +++++++++ blurthing/src/styles/container.rs | 14 ++ blurthing/src/styles/mod.rs | 63 ++++++ blurthing/src/styles/scrollable.rs | 62 ++++++ blurthing/src/styles/slider.rs | 55 ++++++ blurthing/src/styles/text.rs | 11 ++ blurthing/src/styles/text_input.rs | 61 ++++++ blurthing/src/widgets/mod.rs | 16 ++ 11 files changed, 530 insertions(+), 149 deletions(-) create mode 100644 blurthing/src/styles/application.rs create mode 100644 blurthing/src/styles/button.rs create mode 100644 blurthing/src/styles/container.rs create mode 100644 blurthing/src/styles/mod.rs create mode 100644 blurthing/src/styles/scrollable.rs create mode 100644 blurthing/src/styles/slider.rs create mode 100644 blurthing/src/styles/text.rs create mode 100644 blurthing/src/styles/text_input.rs create mode 100644 blurthing/src/widgets/mod.rs diff --git a/blurthing/src/application.rs b/blurthing/src/application.rs index 3d60ca2..a6f0f0b 100644 --- a/blurthing/src/application.rs +++ b/blurthing/src/application.rs @@ -5,14 +5,16 @@ use ::image::imageops::FilterType; use ::image::{DynamicImage, GenericImageView, RgbaImage}; use anyhow::{anyhow, Result}; use iced::alignment::{Horizontal, Vertical}; -use iced::widget::{button, container, image, slider, text, text_input}; -use iced::widget::{column, mouse_area, row, scrollable}; -use iced::{Application, Background, Border, Command, Element, Event, Length, Subscription, Theme}; +use iced::widget::Image; + +use iced::{Application, Command, Event, Length, Subscription}; use native_dialog::{FileDialog, MessageDialog, MessageType}; use super::message::Message; use super::parameters::Parameters; +use super::styles::Theme; use super::undo_history::UndoHistory; +use crate::{styles, widgets::*}; pub const PREVIEW_SIZE: u32 = 512; const IMAGE_DOWNSAMPLE_SIZE: u32 = 128; @@ -50,10 +52,6 @@ impl Application for BlurThing { String::from("BlurThing") } - fn theme(&self) -> Self::Theme { - Theme::TokyoNightLight - } - fn update(&mut self, message: Self::Message) -> Command { match message { Message::SelectImage => { @@ -211,36 +209,31 @@ impl Application for BlurThing { fn view(&self) -> Element { let left: Element = if let Some((_, img)) = &self.computed { - let buffer = img.to_rgba8().to_vec(); - let handle = image::Handle::from_pixels(img.width(), img.height(), buffer); - image(handle).into() + let handle = iced::widget::image::Handle::from_pixels( + img.width(), + img.height(), + img.to_rgba8().to_vec(), + ); + + Image::new(handle).into() } else { - let size = Length::Fixed(PREVIEW_SIZE as f32); - container( - text("Press on \"Select File\" or drop an image here to get started!") - .width(Length::Fill) - .height(Length::Fill) - .vertical_alignment(Vertical::Center) - .horizontal_alignment(Horizontal::Center), - ) - .height(size) - .width(size) - .padding(32) - .style(self.no_image_style()) - .into() + let text = Text::new("No image loaded").size(24); + text.into() }; - let right = column![ - container(self.header()).style(self.container_style()), - container(scrollable(self.controls()).height(Length::Fill)) - .style(self.container_style()), - container(self.footer()).style(self.container_style()) - ]; + let right = Column::new() + .push(Container::new(self.header()).width(Length::Fill)) + .push(Scrollable::new(self.controls()).height(Length::Fill)) + .push(Container::new(self.footer())); - container(row![left, right]).into() + Row::new().push(left).push(right).into() } } +// +// Main Logic +// + impl BlurThing { fn handle_hotkey( key: iced::keyboard::Key, @@ -261,108 +254,6 @@ impl BlurThing { } } - fn header(&self) -> Element { - mouse_area( - column![text(self.title()).size(24), text("by sonodima").size(14)] - .width(Length::Fill) - .padding([16, 24]), - ) - .on_press(Message::OpenProjectRepo) - .into() - } - - fn controls(&self) -> Element { - let x_components = column![ - text("X Components"), - text("Number of samples in the horizontal axis").size(12), - slider(1..=8, self.params.components.0, Message::UpX) - .on_release(Message::SaveParameters) - ]; - - let y_components = column![ - text("Y Components"), - text("Number of samples in the vertical axis").size(12), - slider(1..=8, self.params.components.1, Message::UpY) - .on_release(Message::SaveParameters) - ]; - - let smoothness = column![ - text("Smoothness"), - text("Amount of blur applied before the hash is computed").size(12), - slider(0..=32, self.params.blur, Message::UpBlur).on_release(Message::SaveParameters) - ]; - - let hue_rotation = column![ - text("Hue Rotation"), - text("How much to rotate the hue of the image (color shift)").size(12), - slider(-180..=180, self.params.hue_rotate, Message::UpHue) - .on_release(Message::SaveParameters) - ]; - - let brightness = column![ - text("Brightness"), - text("Adjusts the overall lightness or darkness of the image").size(12), - slider(-100..=100, self.params.brightness, Message::UpBrightness) - .on_release(Message::SaveParameters) - ]; - - let contrast = column![ - text("Contrast"), - text("Modifies the difference between the darkest and lightest parts of the image") - .size(12), - slider(-100..=100, self.params.contrast, Message::UpContrast) - .on_release(Message::SaveParameters) - ]; - - column![ - x_components, - y_components, - smoothness, - hue_rotation, - brightness, - contrast, - ] - .padding(24) - .spacing(8) - .into() - } - - fn footer(&self) -> Element { - let hash_string = self - .computed - .as_ref() - .map(|(hash, _)| hash.clone()) - .unwrap_or_default(); - - let select_file = button( - text("Select File") - .width(Length::Fill) - .horizontal_alignment(Horizontal::Center), - ) - .on_press(Message::SelectImage); - - let mut export_image = button("Export Image"); - if self.computed.is_some() { - export_image = export_image.on_press(Message::ExportImage) - } - - let out_hash = text_input("Load an image to compute its hash", &hash_string) - .on_input(|_| Message::NoOp); - - let mut copy_to_clipboard = button("Copy to Clipboard"); - if self.computed.is_some() { - copy_to_clipboard = copy_to_clipboard.on_press(Message::CopyHashToClipboard) - } - - column![ - row![select_file, export_image].spacing(8), - row![out_hash, copy_to_clipboard].spacing(8) - ] - .padding(16) - .spacing(8) - .into() - } - fn try_load_image(&mut self, path: PathBuf) -> Result<()> { let loaded = ::image::open(&path).map_err(|e| anyhow!(e.to_string().to_lowercase()))?; @@ -429,26 +320,123 @@ impl BlurThing { // Push the initial parameters to the history stack. self.history.push(self.params.clone()); } +} - fn no_image_style(&self) -> container::Appearance { - let background = self.theme().extended_palette().background.strong.color; - let border = self.theme().extended_palette().background.strong.text; - container::Appearance { - background: Some(Background::Color(background)), - text_color: Some(border), - ..Default::default() - } +// +// UI Components +// + +impl BlurThing { + fn header(&self) -> Element { + MouseArea::new( + Column::new() + .push(Text::new(self.title()).size(24)) + .push(Text::new("by sonodima").size(14)) + .padding([16, 24]), + ) + .on_press(Message::OpenProjectRepo) + .into() + } + + fn controls(&self) -> Element { + let x_components = Column::new() + .push(Text::new("X Components")) + .push(Text::new("Number of samples in the horizontal axis").size(12)) + .push( + Slider::new(1..=8, self.params.components.0, Message::UpX) + .on_release(Message::SaveParameters), + ); + + let y_components = Column::new() + .push(Text::new("Y Components")) + .push(Text::new("Number of samples in the vertical axis").size(12)) + .push( + Slider::new(1..=8, self.params.components.1, Message::UpY) + .on_release(Message::SaveParameters), + ); + + let smoothness = Column::new() + .push(Text::new("Smoothness")) + .push(Text::new("Amount of blur applied before the hash is computed").size(12)) + .push( + Slider::new(0..=32, self.params.blur, Message::UpBlur) + .on_release(Message::SaveParameters), + ); + + let hue_rotation = Column::new() + .push(Text::new("Hue Rotation")) + .push(Text::new("How much to rotate the hue of the image (color shift)").size(12)) + .push( + Slider::new(-180..=180, self.params.hue_rotate, Message::UpHue) + .on_release(Message::SaveParameters), + ); + + let brightness = Column::new() + .push(Text::new("Brightness")) + .push(Text::new("Adjusts the overall lightness or darkness of the image").size(12)) + .push( + Slider::new(-100..=100, self.params.brightness, Message::UpBrightness) + .on_release(Message::SaveParameters), + ); + + let contrast = Column::new() + .push(Text::new("Contrast")) + .push( + Text::new( + "Modifies the difference between the darkest and lightest parts of the image", + ) + .size(12), + ) + .push( + Slider::new(-100..=100, self.params.contrast, Message::UpContrast) + .on_release(Message::SaveParameters), + ); + + Column::new() + .push(x_components) + .push(y_components) + .push(smoothness) + .push(hue_rotation) + .push(brightness) + .push(contrast) + .padding(24) + .spacing(8) + .into() } - fn container_style(&self) -> container::Appearance { - let border = self.theme().extended_palette().background.strong.color; - container::Appearance { - border: Border { - color: border, - width: 1.0, - ..Default::default() - }, - ..Default::default() + fn footer(&self) -> Element { + let select_file = Button::new( + Text::new("Select File") + .width(Length::Fill) + .horizontal_alignment(Horizontal::Center), + ) + .on_press(Message::SelectImage); + + let mut export_image = Button::new("Export Image").style(styles::Button::Primary); + if self.computed.is_some() { + export_image = export_image.on_press(Message::ExportImage) + } + + let hash_string = self + .computed + .as_ref() + .map(|(hash, _)| hash.clone()) + .unwrap_or_default(); + let mut out_hash = TextInput::new("Load an image to compute its hash", &hash_string); + if self.computed.is_some() { + out_hash = out_hash.on_input(|_| Message::NoOp); + } + + let mut copy_to_clipboard = Button::new("Copy to Clipboard").style(styles::Button::Primary); + if self.computed.is_some() { + copy_to_clipboard = copy_to_clipboard.on_press(Message::CopyHashToClipboard) } + + Column::new() + .push(Row::new().push(select_file).push(export_image).spacing(8)) + .push(Row::new().push(out_hash).push(copy_to_clipboard).spacing(8)) + .padding(16) + .spacing(8) + .into() } } diff --git a/blurthing/src/main.rs b/blurthing/src/main.rs index cfdc670..c8d632e 100644 --- a/blurthing/src/main.rs +++ b/blurthing/src/main.rs @@ -7,7 +7,9 @@ use application::{BlurThing, PREVIEW_SIZE}; mod application; mod message; mod parameters; +mod styles; mod undo_history; +mod widgets; pub fn main() -> iced::Result { let window = window::Settings { diff --git a/blurthing/src/styles/application.rs b/blurthing/src/styles/application.rs new file mode 100644 index 0000000..3e37fcd --- /dev/null +++ b/blurthing/src/styles/application.rs @@ -0,0 +1,14 @@ +use iced::application::{Appearance, StyleSheet}; + +use super::Theme; + +impl StyleSheet for Theme { + type Style = (); + + fn appearance(&self, _style: &Self::Style) -> Appearance { + Appearance { + background_color: self.palette.background, + text_color: self.palette.foreground, + } + } +} diff --git a/blurthing/src/styles/button.rs b/blurthing/src/styles/button.rs new file mode 100644 index 0000000..2bf3062 --- /dev/null +++ b/blurthing/src/styles/button.rs @@ -0,0 +1,95 @@ +use iced::widget::button::{Appearance, StyleSheet}; +use iced::{color, Background, Border, Shadow}; + +use super::Theme; + +#[derive(Debug, Clone, Copy, Default)] +pub enum Button { + #[default] + Default, + Primary, +} + +impl StyleSheet for Theme { + type Style = Button; + + fn active(&self, style: &Self::Style) -> Appearance { + let background = match style { + Button::Default => self.palette.base_200.into(), + Button::Primary => self.palette.primary_100.into(), + }; + + Appearance { + shadow_offset: [0.0, 0.0].into(), + background: Some(background), + text_color: match style { + Button::Default => self.palette.base_content, + Button::Primary => self.palette.primary_content, + }, + border: Border { + color: match style { + Button::Default => self.palette.base_400, + Button::Primary => self.palette.primary_500, + }, + width: 1.0, + radius: 6.0.into(), + }, + shadow: Shadow { + color: color!(0x000000, 0.3), + offset: [0.0, 0.0].into(), + blur_radius: 8.0, + }, + } + } + + fn disabled(&self, style: &Self::Style) -> Appearance { + self.active(style) // TODO + } + + fn hovered(&self, style: &Self::Style) -> Appearance { + let base = self.active(style); + + let background = match style { + Button::Default => self.palette.base_300.into(), + Button::Primary => self.palette.primary_200.into(), + }; + + Appearance { + background: Some(background), + border: Border { + color: match style { + Button::Default => self.palette.base_400, + Button::Primary => self.palette.primary_500, + }, + ..base.border + }, + shadow: Shadow { + color: color!(0x000000, 0.3), + offset: [0.0, 3.0].into(), + blur_radius: 12.0, + }, + ..base + } + } + + fn pressed(&self, style: &Self::Style) -> Appearance { + let base = self.hovered(style); + + let background = match style { + Button::Default => self.palette.base_400.into(), + Button::Primary => self.palette.primary_300.into(), + }; + + Appearance { + background: Some(background), + border: Border { + color: match style { + Button::Default => self.palette.base_400, + Button::Primary => self.palette.primary_500, + }, + ..base.border + }, + ..base + } + } +} diff --git a/blurthing/src/styles/container.rs b/blurthing/src/styles/container.rs new file mode 100644 index 0000000..b43379a --- /dev/null +++ b/blurthing/src/styles/container.rs @@ -0,0 +1,14 @@ +use iced::widget::container::{Appearance, StyleSheet}; + +use super::Theme; + +impl StyleSheet for Theme { + type Style = (); + + fn appearance(&self, _style: &Self::Style) -> Appearance { + Appearance { + background: Some(self.palette.base_100.into()), + ..Default::default() + } + } +} diff --git a/blurthing/src/styles/mod.rs b/blurthing/src/styles/mod.rs new file mode 100644 index 0000000..2e3de41 --- /dev/null +++ b/blurthing/src/styles/mod.rs @@ -0,0 +1,63 @@ +mod application; +mod button; +mod container; +mod scrollable; +mod slider; +mod text; +mod text_input; + +use iced::{color, Color}; + +pub use button::Button; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Palette { + pub background: Color, + pub foreground: Color, + + pub base_100: Color, + pub base_200: Color, + pub base_300: Color, + pub base_400: Color, + pub base_500: Color, + pub base_content: Color, + + pub primary_100: Color, + pub primary_200: Color, + pub primary_300: Color, + pub primary_500: Color, + pub primary_content: Color, +} + +impl Palette { + pub const DARK: Self = Self { + background: color!(0x060606), + foreground: color!(0xd6d6d6), + + base_100: color!(0x121212), + base_200: color!(0x1e1e1e), + base_300: color!(0x2b2b2b), + base_400: color!(0x383838), + base_500: color!(0x454545), + base_content: color!(0xd6d6d6), + + primary_100: color!(0x297aa3), + primary_200: color!(0x2e8ab8), + primary_300: color!(0x3399cc), + primary_500: color!(0x5cadd6), + primary_content: color!(0xffffff), + }; +} + +#[derive(Debug, Clone, PartialEq)] +pub struct Theme { + palette: Palette, +} + +impl Default for Theme { + fn default() -> Self { + Self { + palette: Palette::DARK, + } + } +} diff --git a/blurthing/src/styles/scrollable.rs b/blurthing/src/styles/scrollable.rs new file mode 100644 index 0000000..e94f970 --- /dev/null +++ b/blurthing/src/styles/scrollable.rs @@ -0,0 +1,62 @@ +use iced::widget::scrollable::{Appearance, Scrollbar, Scroller, StyleSheet}; +use iced::Border; + +use super::Theme; + +impl StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: &Self::Style) -> Appearance { + Appearance { + container: Default::default(), + scrollbar: Scrollbar { + background: None, + border: Border::default(), + scroller: Scroller { + color: self.palette.base_300, + border: Border { + color: self.palette.base_500, + width: 1.0, + radius: 4.0.into(), + }, + }, + }, + gap: None, + } + } + + fn hovered(&self, _style: &Self::Style, is_mouse_over_scrollbar: bool) -> Appearance { + let base = self.active(_style); + + Appearance { + scrollbar: Scrollbar { + scroller: Scroller { + color: if is_mouse_over_scrollbar { + self.palette.base_400 + } else { + self.palette.base_300 + }, + ..base.scrollbar.scroller + }, + ..base.scrollbar + }, + + ..base + } + } + + fn dragging(&self, _style: &Self::Style) -> Appearance { + let base = self.hovered(_style, true); + + Appearance { + scrollbar: Scrollbar { + scroller: Scroller { + color: self.palette.base_500, + ..base.scrollbar.scroller + }, + ..base.scrollbar + }, + ..base + } + } +} diff --git a/blurthing/src/styles/slider.rs b/blurthing/src/styles/slider.rs new file mode 100644 index 0000000..778f554 --- /dev/null +++ b/blurthing/src/styles/slider.rs @@ -0,0 +1,55 @@ +use iced::widget::slider::{Appearance, Handle, HandleShape, Rail, StyleSheet}; +use iced::Color; + +use super::Theme; + +impl StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: &Self::Style) -> Appearance { + Appearance { + rail: Rail { + colors: (self.palette.primary_200, self.palette.base_300), + width: 4.0, + border_radius: 2.0.into(), + }, + handle: Handle { + shape: HandleShape::Rectangle { + width: 6, + border_radius: 3.0.into(), + }, + color: self.palette.primary_300, + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + } + } + + fn hovered(&self, _style: &Self::Style) -> Appearance { + let base = self.active(_style); + + Appearance { + rail: Rail { + colors: (self.palette.primary_300, self.palette.base_400), + ..base.rail + }, + handle: Handle { + color: self.palette.primary_500, + ..base.handle + }, + ..base + } + } + + fn dragging(&self, style: &Self::Style) -> Appearance { + let base = self.hovered(style); + + Appearance { + rail: Rail { + colors: (self.palette.primary_500, self.palette.base_500), + ..base.rail + }, + ..base + } + } +} diff --git a/blurthing/src/styles/text.rs b/blurthing/src/styles/text.rs new file mode 100644 index 0000000..d70602d --- /dev/null +++ b/blurthing/src/styles/text.rs @@ -0,0 +1,11 @@ +use iced::widget::text::{Appearance, StyleSheet}; + +use super::Theme; + +impl StyleSheet for Theme { + type Style = (); + + fn appearance(&self, _style: Self::Style) -> Appearance { + Default::default() + } +} diff --git a/blurthing/src/styles/text_input.rs b/blurthing/src/styles/text_input.rs new file mode 100644 index 0000000..5eceb5b --- /dev/null +++ b/blurthing/src/styles/text_input.rs @@ -0,0 +1,61 @@ +use iced::widget::text_input::{Appearance, StyleSheet}; +use iced::{Background, Border, Color}; + +use super::Theme; + +impl StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: &Self::Style) -> Appearance { + Appearance { + background: self.palette.base_200.into(), + border: Border { + color: self.palette.base_400, + width: 1.0, + radius: 6.0.into(), + }, + icon_color: self.palette.base_content, + } + } + + fn disabled(&self, _style: &Self::Style) -> Appearance { + Appearance { + background: self.palette.base_300.into(), // TODO: change + ..self.active(_style) + } + } + + fn hovered(&self, _style: &Self::Style) -> Appearance { + self.active(_style) + } + + fn focused(&self, _style: &Self::Style) -> Appearance { + let base = self.active(_style); + + Appearance { + border: Border { + color: self.palette.primary_200, + ..base.border + }, + ..base + } + } + + fn disabled_color(&self, _style: &Self::Style) -> Color { + self.palette.base_400 // todo change + } + + fn placeholder_color(&self, _style: &Self::Style) -> Color { + self.palette.base_400 + } + + fn selection_color(&self, _style: &Self::Style) -> Color { + let mut color = self.palette.primary_100; + color.a = 0.2; + color + } + + fn value_color(&self, _style: &Self::Style) -> Color { + self.palette.base_content + } +} diff --git a/blurthing/src/widgets/mod.rs b/blurthing/src/widgets/mod.rs new file mode 100644 index 0000000..e166963 --- /dev/null +++ b/blurthing/src/widgets/mod.rs @@ -0,0 +1,16 @@ +use crate::styles::Theme; + +// Export iced's native widgets with our custom theme. When you need to use a +// widget in your application, you should use this module instead of iced's. + +pub type Element<'a, Message> = iced::Element<'a, Message, Theme>; +pub type Container<'a, Message> = iced::widget::Container<'a, Message, Theme>; +pub type Column<'a, Message> = iced::widget::Column<'a, Message, Theme>; +pub type Row<'a, Message> = iced::widget::Row<'a, Message, Theme>; +pub type MouseArea<'a, Message> = iced::widget::MouseArea<'a, Message, Theme>; +pub type Scrollable<'a, Message> = iced::widget::Scrollable<'a, Message, Theme>; + +pub type Button<'a, Message> = iced::widget::Button<'a, Message, Theme>; +pub type Slider<'a, T, Message> = iced::widget::Slider<'a, T, Message, Theme>; +pub type TextInput<'a, Message> = iced::widget::TextInput<'a, Message, Theme>; +pub type Text<'a> = iced::widget::Text<'a, Theme>;