From ec99e817c1b0cebaeb60f2722ac6cbfa2642418a Mon Sep 17 00:00:00 2001 From: Candifloss Date: Tue, 25 Nov 2025 23:18:04 +0530 Subject: [PATCH] Switch back to Slint - It's simply simpler in Slint --- README.md | 2 +- widget/Cargo.toml | 4 +- widget/build.rs | 3 + widget/src/main.rs | 34 +++++- widget/src/show_popup.rs | 228 ++++++----------------------------- widget/ui/widget-popup.slint | 139 +++++++++++++++++++++ 6 files changed, 212 insertions(+), 198 deletions(-) create mode 100644 widget/build.rs create mode 100644 widget/ui/widget-popup.slint diff --git a/README.md b/README.md index 0b3c64f..dbffd1d 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ lang = "en" - [ ] Complete library - [ ] Complete configuration - [ ] CLI args -- [ ] GUI - [iced](https://book.iced.rs/) +- [ ] GUI - ~~[iced](https://book.iced.rs/)~~ [Slint](https://docs.slint.dev/latest/docs/slint/) - [ ] Icons - [ ] Support One Call API 3.0 diff --git a/widget/Cargo.toml b/widget/Cargo.toml index 3b16737..07986bf 100644 --- a/widget/Cargo.toml +++ b/widget/Cargo.toml @@ -10,5 +10,7 @@ reqwest = {version = "0.12.23", features = ["blocking", "json"] } toml = "0.9.7" dirs = "6.0.0" serde_json = "1.0.145" -iced = { version = "0.13.1", features = ["advanced"] } +slint = "1.14.1" +[build-dependencies] +slint-build = "1.14.1" diff --git a/widget/build.rs b/widget/build.rs new file mode 100644 index 0000000..cc515dc --- /dev/null +++ b/widget/build.rs @@ -0,0 +1,3 @@ +fn main() { + slint_build::compile("ui/widget-popup.slint").unwrap(); +} diff --git a/widget/src/main.rs b/widget/src/main.rs index b212ae6..4d29fc1 100644 --- a/widget/src/main.rs +++ b/widget/src/main.rs @@ -22,9 +22,39 @@ fn main() -> Result<(), Box> { .join("candydesktop/owm_widget.json"); let json_data = fs::read_to_string(&cache_path)?; - let weather_data: WeatherResponse = serde_json::from_str(&json_data)?; + let resp: WeatherResponse = serde_json::from_str(&json_data)?; - show_popup::show_popup(weather_data, cfg)?; + let city = resp.name.clone().unwrap_or_else(|| "Unknown".into()); + let country = resp + .sys + .as_ref() + .and_then(|s| s.country.clone()) + .unwrap_or_else(|| String::new()); + + let (weather_main, weather_description, icon) = if let Some(w) = resp.weather.first() { + (w.main.clone(), w.description.clone(), w.icon.clone()) + } else { + ("N/A".into(), "N/A".into(), String::new()) + }; + + let temp = resp.main.as_ref().and_then(|m| m.temp).unwrap_or(0.0); + let temperature = format!("{temp:.1}"); + let unit = match cfg.query_params.units.as_str() { + "metric" => 'C', + "imperial" => 'F', + "standard" => 'K', + _ => '?', + }; + + show_popup::show_popup( + city, + country, + weather_main, + weather_description, + icon, + temperature, + unit, + )?; } other => return Err(format!("Unsupported api_version: {other:?}").into()), } diff --git a/widget/src/show_popup.rs b/widget/src/show_popup.rs index 5078095..3c3e50a 100644 --- a/widget/src/show_popup.rs +++ b/widget/src/show_popup.rs @@ -1,203 +1,43 @@ -use iced::{ - Background, Border, Color, Font, Length, Point, Shadow, Size, Task, - alignment::{Horizontal, Vertical}, - application::Appearance, - border, - font::Family, - widget::{Column, Row, Text, container}, - window, -}; -use owm_rs::free_api_v25::current::WeatherResponse; -use owm_widg_config::config::Config; +use slint::SharedString; -#[derive(Debug, Clone)] -enum Message {} +slint::include_modules!(); -struct WeatherPopup { - resp: WeatherResponse, - conf: Config, -} - -impl WeatherPopup { - fn new(resp: WeatherResponse, conf: Config) -> Self { - Self { resp, conf } - } - - fn update(&mut self, _message: Message) -> Task { - Task::none() - } - - fn view(&self) -> iced::Element { - // Extract data - let city = self.resp.name.clone().unwrap_or_else(|| "Unknown".into()); - let country = self - .resp - .sys - .as_ref() - .and_then(|s| s.country.clone()) - .unwrap_or_default(); - let (main, desc, icon) = self - .resp - .weather - .first() - .map_or(("N/A".into(), "N/A".into(), String::new()), |w| { - (w.main.clone(), w.description.clone(), w.icon.clone()) - }); - let temp = self.resp.main.as_ref().and_then(|m| m.temp).unwrap_or(0.0); - let temperature = format!("{temp:.1}"); - let unit = match self.conf.query_params.units.as_str() { - "metric" => 'C', - "imperial" => 'F', - "standard" => 'K', - _ => '?', - }; - - let font = Font { - family: Family::Name("IosevkaTermSlab Nerd Font Mono"), - ..Font::DEFAULT - }; - - // Top layout - let icon_text = Text::new(icon_to_nerd_font(&icon)) - .font(font) - .size(88) - .height(88) - .width(88) - .align_x(Horizontal::Left) - .align_y(Vertical::Center) - .color(Color::WHITE); - let icon_block = Column::new().push(icon_text).height(88).width(88); - - let location_text = Text::new(format!("{city}, {country}")) - .font(font) - .size(16) - .color(Color::WHITE) - .align_x(Horizontal::Right) - .width(192) - .height(26); - - let temp_text = Text::new(temperature) - .font(font) - .size(46) - .align_x(Horizontal::Right) - .align_y(Vertical::Center) - .height(52) - .width(Length::Fill) - .color(Color::WHITE); - let temp_val_column = Column::new().push(temp_text).height(52).width(Length::Fill); - let degree_text = Text::new("o") - .font(font) - .size(16) - .color(Color::WHITE) - .height(23) - .align_y(Vertical::Bottom); - let unit_text = Text::new(format!("{unit}")) - .font(font) - .size(16) - .color(Color::WHITE) - .height(23) - .align_y(Vertical::Bottom); - let unit_block = Column::with_children(vec![ - Row::new().push(degree_text).height(23).into(), - Row::new().push(unit_text).height(23).into(), - ]); - - let temp_block = Row::new() - .push(temp_val_column) - .push(Column::new().push(unit_block).height(52)) - .align_y(Vertical::Center) - .height(52) - .width(192); - - let right_block = Column::new() - .push(location_text) - .push(temp_block) - .width(192) - .height(Length::Fill); - - let top_row = Row::new() - .push(icon_block) - .push(right_block) - .width(Length::Fill) - .height(88); - - // Bottom layout - let main_text = Text::new(main) - .font(font) - .size(28) - .color(Color::WHITE) - .align_y(Vertical::Top) - .align_x(Horizontal::Left) - .width(Length::FillPortion(3)); - - let desc_text = Text::new(desc) - .font(font) - .size(16) - .align_x(Horizontal::Right) - .align_y(Vertical::Top) - .color(Color::from_rgba(1.0, 1.0, 1.0, 0.8)) - .width(Length::FillPortion(4)); - - let bottom_row = Row::new() - .push(main_text) - .push(desc_text) - .width(Length::Fill) - .height(52); - - // Combine - let content = Column::new() - .push(top_row) - .push(bottom_row) - .spacing(6) - .width(Length::Fill) - .height(Length::Fill); - - container(content) - .style(|_theme| iced::widget::container::Style { - background: Some(Background::from(Color::from_rgba(0.20, 0.43, 0.55, 0.25))), - border: Border { - color: Color::TRANSPARENT, - width: 0.0, - radius: border::Radius::from(10.0), - }, - shadow: Shadow::default(), - text_color: Some(Color::WHITE), - }) - .padding(10) - .into() - } -} - -pub fn show_popup(resp: WeatherResponse, conf: Config) -> iced::Result { - iced::application("Weather Popup", WeatherPopup::update, WeatherPopup::view) - .window(window::Settings { - size: Size { - width: 300.0, - height: 150.0, - }, - position: window::Position::Specific(Point { x: 20.0, y: 20.0 }), - decorations: false, - ..window::Settings::default() - }) - .style(|_, _| Appearance { - background_color: Color::TRANSPARENT, - text_color: Color::WHITE, - }) - .run_with(move || (WeatherPopup::new(resp, conf), Task::none())) +/// Create and show the UI window populated with weather data. +pub fn show_popup( + city: String, + country: String, + weather_main: String, + weather_description: String, + icon_code: String, + temperature: String, + unit: char, +) -> Result<(), Box> { + let ui = MainWindow::new()?; + ui.set_city(SharedString::from(city)); + ui.set_country(SharedString::from(country)); + ui.set_weather_main(SharedString::from(weather_main)); + ui.set_weather_description(SharedString::from(weather_description)); + ui.set_weather_icon(SharedString::from(icon_to_nerd_font(&icon_code))); + ui.set_temperature(SharedString::from(temperature)); + ui.set_unit(SharedString::from(unit)); + + ui.run()?; + Ok(()) } +/// Convert OWM icon codes (e.g. "01d", "09n") to Nerd Font weather glyphs. fn icon_to_nerd_font(code: &str) -> String { match code { - "01d" => "", - "01n" => "", - "02d" | "02n" => "", - "03d" | "03n" => "", - "04d" | "04n" => "", - "09d" | "09n" => "", - "10d" | "10n" => "", - "11d" | "11n" => "", - "13d" | "13n" => "", - "50d" | "50n" => "", + "01d" => "", // clear day + "01n" => "", // clear night + "02d" | "02n" => "", // few clouds + "03d" | "03n" => "", // scattered clouds + "04d" | "04n" => "", // broken clouds + "09d" | "09n" => "", // shower rain + "10d" | "10n" => "", // rain + "11d" | "11n" => "", // thunderstorm + "13d" | "13n" => "", // snow + "50d" | "50n" => "", // mist _ => "", } .into() diff --git a/widget/ui/widget-popup.slint b/widget/ui/widget-popup.slint new file mode 100644 index 0000000..e1ce605 --- /dev/null +++ b/widget/ui/widget-popup.slint @@ -0,0 +1,139 @@ +export component MainWindow inherits Window { + + in property city; + in property country; + in property weather_main; + in property weather_description; + in property weather_icon; + in property temperature; + in property unit; + + always-on-top: true; + no-frame: true; + width: 300px; + height: 124px; + background: transparent; + default-font-family: "IosevkaTermSlab Nerd Font Mono"; + + weather_popup := Rectangle { + width: 100%; + height: 100%; + border-radius: 12px; + background: @linear-gradient(160deg, #236a8bc7, #c5edff5c); + drop-shadow-blur: 15px; + drop-shadow-color: #00000066; + + VerticalLayout { + spacing: 2px; // Consistent vertical spacing + horizontal-stretch: 1.0; + padding: 12px; // Consistent padding throughout + + /* Top: City */ + Text { + text: city + ", " + country; + font-size: 14px; + color: #fff; + //font-family: + horizontal-alignment: TextHorizontalAlignment.left; + height: 18px; // Fixed height for consistent spacing + font-weight: 600; + } + + /* Middle: icon on left, temp on right */ + HorizontalLayout { + horizontal-stretch: 1.0; + height: 50px; // Fixed height for main content area + padding: 0px; + + /* Icon */ + Text { + text: weather_icon; + font-size: 40px; + horizontal-alignment: TextHorizontalAlignment.left; + color: #fff; + vertical-alignment: TextVerticalAlignment.center; + width: 46px; // Fixed width for consistent alignment + } + + /* Flexible spacer to help with alignment */ + Rectangle { + horizontal-stretch: 1.0; + background: transparent; + } + + /* Temperature group */ + HorizontalLayout { + spacing: 4px; // Tighter spacing for temperature elements + padding: 0px; + //height: 50px; + + Text { + text: temperature; + font-size: 40px; + color: #fff; + vertical-alignment: TextVerticalAlignment.center; + font-weight: 900; + } + + VerticalLayout { + height: 50px; // Match temperature text height + padding: 0px; + spacing: 0px; + + Text { + text: "o"; + font-size: 16px; + color: #fff; + horizontal-alignment: TextHorizontalAlignment.left; + vertical-alignment: TextVerticalAlignment.center; + height: 25px; + font-weight: 800; + } + Text { + text: unit; + font-size: 16px; + color: #fff; + horizontal-alignment: TextHorizontalAlignment.left; + vertical-alignment: TextVerticalAlignment.center; + height: 20px; + width: 20px; + font-weight: 800; + } + } + } + } + + /* Bottom: summary + description */ + HorizontalLayout { + spacing: 8px; + horizontal-stretch: 1.0; + height: 32px; + padding: 0px; + + /* Summary */ + Text { + text: weather_main; + font-size: 18px; + color: #fff; + vertical-alignment: TextVerticalAlignment.top; + horizontal-alignment: TextHorizontalAlignment.left; + width: 40%; // Fixed percentage width for consistent layout + font-weight: 800; + } + + /* Description */ + Text { + text: weather_description; + font-size: 12px; + color: #ffffffcc; + horizontal-alignment: TextHorizontalAlignment.right; + vertical-alignment: TextVerticalAlignment.top; + horizontal-stretch: 1.0; + wrap: word-wrap; + height: 100%; + font-weight: 600; + } + } + } + } +} \ No newline at end of file