From 91149c0e06915bd83a5ad96729dae5668ac0e37f Mon Sep 17 00:00:00 2001 From: Bono Fox Date: Sun, 19 Oct 2025 20:22:34 +0200 Subject: [PATCH 01/10] =?UTF-8?q?=E2=9C=A8=20Defined=20multiple=20start=20?= =?UTF-8?q?modes=20(window=20sizes)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GUI 2.0 will be available as a dashboard and as a widget - dashboard brings the full number of functionalities - widget brings a smaller size and only the important features --- README.md | 8 ++++++++ src/gui/main.rs | 44 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 43837f8..e484afd 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,14 @@ The way you use Tiny4Linux depends on the variant you chose: You can start the GUI by clicking on the icon in your desktop manager or by typing `tiny4linux-gui` in your terminal. You'll have full access to all available features in the window that opens. +The UI is available in two variants: + +1. The **dashboard** is a full overview about the current state and all the available controlling functions. +Via the terminal, you can open it by setting the flag: `tiny4linux-gui --start-as dashboard` +2. The **widget** is a smaller version intended for permanent viewing. +It contains only the more important functions you might need more often. +Via the terminal, you can open it by setting the flag: `tiny4linux-gui --start-as widget` + ### CLI You can use the CLI by typing `t4l` or `tiny4linux-cli` in your terminal. diff --git a/src/gui/main.rs b/src/gui/main.rs index 97ef7a9..43fbace 100644 --- a/src/gui/main.rs +++ b/src/gui/main.rs @@ -385,11 +385,53 @@ impl MainPanel { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum StartMode { + Dashboard, + Widget, + Invalid, +} + +fn get_start_mode() -> StartMode { + let args: Vec = std::env::args().collect(); + + if let Some(start_mode_flag_pos) = args.iter().position(|a| a == ("--start-as")) { + if let Some(start_mode_arg) = args.get(start_mode_flag_pos + 1) { + return if start_mode_arg.eq_ignore_ascii_case("dashboard") { + StartMode::Dashboard + } else if start_mode_arg.eq_ignore_ascii_case("widget") { + StartMode::Widget + } else { + StartMode::Invalid + }; + } + } + + StartMode::Dashboard +} + fn main() -> iced::Result { + let start_mode = get_start_mode(); + + if start_mode == StartMode::Invalid { + println!( + "Invalid start mode. Please use --start-as dashboard or --start-as widget or remove the flag." + ); + panic!(); + } + + println!("Starting Tiny4Linux in {:?} mode", start_mode); + + let window_size = match start_mode { + StartMode::Dashboard => Size::new(860.0, 720.0), // 43:36 + StartMode::Widget => Size::new(300.0, 550.0), // 6:11 + StartMode::Invalid => Size::ZERO, + }; + iced::application("Tiny4Linux", MainPanel::update, MainPanel::view) .theme(|_| obsbot_theme()) .window(window::Settings { - size: Size::new(300.0, 900.0), + size: window_size, resizable: false, decorations: true, ..Default::default() From a53fbb9f783d0302608eed7317766d205484d6bf Mon Sep 17 00:00:00 2001 From: Bono Fox Date: Wed, 29 Oct 2025 18:59:47 +0100 Subject: [PATCH 02/10] =?UTF-8?q?=F0=9F=9A=B8=20Separate=20Desktop=20Appli?= =?UTF-8?q?cation=20for=20Widget=20Mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - users might want to always start in one of the modes - with this change, two desktop applications are placed - one opens in dashboard mode, the other in widget mode --- deployment/gui/aur/PKGBUILD | 3 ++- ...tiny4linux.desktop => tiny4linux-dashboard.desktop} | 0 src/gui/tiny4linux-widget.desktop | 10 ++++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) rename src/gui/{tiny4linux.desktop => tiny4linux-dashboard.desktop} (100%) create mode 100644 src/gui/tiny4linux-widget.desktop diff --git a/deployment/gui/aur/PKGBUILD b/deployment/gui/aur/PKGBUILD index 736717c..f86a7c0 100644 --- a/deployment/gui/aur/PKGBUILD +++ b/deployment/gui/aur/PKGBUILD @@ -21,5 +21,6 @@ package() { install -Dm755 "target/release/$pkgname" "$pkgdir/usr/bin/$pkgname" install -Dm755 "target/release/assets/icon.png" "$pkgdir/usr/share/icons/hicolor/256x256/apps/tiny4linux.png" - install -Dm755 "src/gui/tiny4linux.desktop" "$pkgdir/usr/share/applications/tiny4linux.desktop" + install -Dm755 "src/gui/tiny4linux-dashboard.desktop" "$pkgdir/usr/share/applications/tiny4linux-dashboard.desktop" + install -Dm755 "src/gui/tiny4linux-widget.desktop" "$pkgdir/usr/share/applications/tiny4linux-widget.desktop" } \ No newline at end of file diff --git a/src/gui/tiny4linux.desktop b/src/gui/tiny4linux-dashboard.desktop similarity index 100% rename from src/gui/tiny4linux.desktop rename to src/gui/tiny4linux-dashboard.desktop diff --git a/src/gui/tiny4linux-widget.desktop b/src/gui/tiny4linux-widget.desktop new file mode 100644 index 0000000..7b3ed60 --- /dev/null +++ b/src/gui/tiny4linux-widget.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Version=1.0 +Type=Application +Name=Tiny4Linux (Widget) +Comment=A graphical user interface for OBSBOT Tiny devices opened in widget mode +Exec=tiny4linux-gui --start-as widget +Icon=tiny4linux +Terminal=false +Categories=Utility;System; +Keywords=linux;obsbot;tiny;utility; \ No newline at end of file From 0d764e1e0b4d9aac8186ebbbbd4e27b279da50fa Mon Sep 17 00:00:00 2001 From: Bono Fox Date: Thu, 30 Oct 2025 15:40:18 +0100 Subject: [PATCH 03/10] =?UTF-8?q?=E2=9E=95=20Added=20Font=20Awesome=20Icon?= =?UTF-8?q?s=20for=20Iced?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 37 +++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 ++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 628de84..285a82c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1621,6 +1621,17 @@ dependencies = [ "web-time", ] +[[package]] +name = "iced_font_awesome" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "810ef61d81cd4e164a540a043bd080495f8906981bca263cbe7dacb0d7f1a6f0" +dependencies = [ + "iced", + "serde", + "serde_json", +] + [[package]] name = "iced_futures" version = "0.13.2" @@ -1811,6 +1822,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "jni" version = "0.21.1" @@ -2926,6 +2943,12 @@ dependencies = [ "unicode-script", ] +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "same-file" version = "1.0.6" @@ -2996,6 +3019,19 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -3368,6 +3404,7 @@ dependencies = [ "hex", "hexdump", "iced", + "iced_font_awesome", "nix 0.30.1", "thiserror 2.0.17", "tiny4linux_assets", diff --git a/Cargo.toml b/Cargo.toml index 54ceb86..3b844ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,12 +14,13 @@ keywords = ["obsbot", "tiny2", "camera", "webcam", "linux"] categories = ["hardware-support"] [features] -gui = ["iced", "tiny4linux_assets"] +gui = ["iced", "iced_font_awesome", "tiny4linux_assets"] cli = ["clap", "clap_complete", "dialoguer"] [dependencies] tiny4linux_assets = { git = "https://github.com/OpenFoxes/Tiny4Linux-Icons", rev = "ba11e78", optional = true } iced = { version = "0.13.0", features = ["image", "tokio"], optional = true } +iced_font_awesome = { version = "0.2.1", optional = true } nix = { version = "0.30.0", features = ["ioctl"] } errno = "0.3.14" hex = "0.4.3" From 095cd99b574f79bcdddb99bbda00941615de4b36 Mon Sep 17 00:00:00 2001 From: Bono Fox Date: Thu, 30 Oct 2025 15:42:41 +0100 Subject: [PATCH 04/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Renamed=20WindowMode?= =?UTF-8?q?=20Enum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - values should be switchable in runtime - therefore, it should not only describe the startup --- src/gui/main.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/gui/main.rs b/src/gui/main.rs index 43fbace..bf02157 100644 --- a/src/gui/main.rs +++ b/src/gui/main.rs @@ -386,34 +386,34 @@ impl MainPanel { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum StartMode { +enum WindowMode { Dashboard, Widget, Invalid, } -fn get_start_mode() -> StartMode { +fn get_start_mode() -> WindowMode { let args: Vec = std::env::args().collect(); if let Some(start_mode_flag_pos) = args.iter().position(|a| a == ("--start-as")) { if let Some(start_mode_arg) = args.get(start_mode_flag_pos + 1) { return if start_mode_arg.eq_ignore_ascii_case("dashboard") { - StartMode::Dashboard + WindowMode::Dashboard } else if start_mode_arg.eq_ignore_ascii_case("widget") { - StartMode::Widget + WindowMode::Widget } else { - StartMode::Invalid + WindowMode::Invalid }; } } - StartMode::Dashboard + WindowMode::Dashboard } fn main() -> iced::Result { let start_mode = get_start_mode(); - if start_mode == StartMode::Invalid { + if start_mode == WindowMode::Invalid { println!( "Invalid start mode. Please use --start-as dashboard or --start-as widget or remove the flag." ); @@ -423,9 +423,9 @@ fn main() -> iced::Result { println!("Starting Tiny4Linux in {:?} mode", start_mode); let window_size = match start_mode { - StartMode::Dashboard => Size::new(860.0, 720.0), // 43:36 - StartMode::Widget => Size::new(300.0, 550.0), // 6:11 - StartMode::Invalid => Size::ZERO, + WindowMode::Dashboard => Size::new(860.0, 720.0), // 43:36 + WindowMode::Widget => Size::new(300.0, 550.0), // 6:11 + WindowMode::Invalid => Size::ZERO, }; iced::application("Tiny4Linux", MainPanel::update, MainPanel::view) From dc27014837ae315f14902085401cb6f132f33099 Mon Sep 17 00:00:00 2001 From: Bono Fox Date: Thu, 30 Oct 2025 15:44:37 +0100 Subject: [PATCH 05/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Extracted=20the=20re?= =?UTF-8?q?quest=20for=20the=20window=20settings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - makes it possible to request the dynamic window settings in run time --- src/gui/main.rs | 50 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/src/gui/main.rs b/src/gui/main.rs index bf02157..aa0ffc8 100644 --- a/src/gui/main.rs +++ b/src/gui/main.rs @@ -1,11 +1,12 @@ mod styles; use crate::styles::theme::obsbot_theme; -use iced::Element; use iced::alignment::{Horizontal, Vertical}; use iced::widget::button::{primary, secondary}; use iced::widget::{button, column, image, row, text, text_input, toggler}; +use iced::window::Position; use iced::{Alignment, Length, Padding, Size, Subscription, Task, time, window}; +use iced::{Element, Point}; use std::time::Duration; use tiny4linux::{AIMode, Camera, ExposureMode, OBSBotWebCam, SleepMode, TrackingSpeed}; use tiny4linux_assets::handle_t4l_asset; @@ -392,6 +393,38 @@ enum WindowMode { Invalid, } +fn get_size_for_window_mode(window_mode: WindowMode) -> Size { + match window_mode { + WindowMode::Dashboard => Size::new(860.0, 720.0), // 43:36 + WindowMode::Widget => Size::new(300.0, 550.0), // 6:11 + WindowMode::Invalid => Size::ZERO, + } +} + +fn get_position_for_window_mode(window_mode: WindowMode) -> Position { + match window_mode { + WindowMode::Dashboard => Position::Centered, + WindowMode::Widget => Position::SpecificWith(|window_size, screen_size| Point { + x: (screen_size.width - window_size.width), + y: (screen_size.height - window_size.height), + }), + WindowMode::Invalid => Position::Centered, + } +} + +fn get_window_settings_for_window_mode(window_mode: WindowMode) -> window::Settings { + let window_size = get_size_for_window_mode(window_mode); + window::Settings { + size: window_size, + resizable: false, + min_size: Some(window_size), + max_size: Some(window_size), + position: get_position_for_window_mode(window_mode), + decorations: true, + ..Default::default() + } +} + fn get_start_mode() -> WindowMode { let args: Vec = std::env::args().collect(); @@ -422,20 +455,9 @@ fn main() -> iced::Result { println!("Starting Tiny4Linux in {:?} mode", start_mode); - let window_size = match start_mode { - WindowMode::Dashboard => Size::new(860.0, 720.0), // 43:36 - WindowMode::Widget => Size::new(300.0, 550.0), // 6:11 - WindowMode::Invalid => Size::ZERO, - }; - iced::application("Tiny4Linux", MainPanel::update, MainPanel::view) .theme(|_| obsbot_theme()) - .window(window::Settings { - size: window_size, - resizable: false, - decorations: true, - ..Default::default() - }) + .window(get_window_settings_for_window_mode(start_mode)) .subscription(MainPanel::subscription) - .run_with(|| MainPanel::init_state()) + .run_with(move || MainPanel::init_state()) } From 1a60770273b2850b602390011b04814ba4a30ac0 Mon Sep 17 00:00:00 2001 From: Bono Fox Date: Thu, 30 Oct 2025 15:53:03 +0100 Subject: [PATCH 06/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Saving=20the=20ID=20?= =?UTF-8?q?of=20the=20current=20window?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - make it possible to reopen the window for reopening on new window mode --- src/gui/main.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/gui/main.rs b/src/gui/main.rs index aa0ffc8..9aabbda 100644 --- a/src/gui/main.rs +++ b/src/gui/main.rs @@ -13,6 +13,7 @@ use tiny4linux_assets::handle_t4l_asset; #[derive(Debug, Clone, PartialEq)] enum Message { + ChangeMainWindowId(Option), ChangeSleeping(bool), ChangeTracking(AIMode), ChangeTrackingSpeed(TrackingSpeed), @@ -31,6 +32,7 @@ enum Message { struct MainPanel { camera: Option, + main_window_id: Option, awake: SleepMode, tracking: AIMode, tracking_speed: TrackingSpeed, @@ -52,6 +54,7 @@ impl MainPanel { ( MainPanel { camera, + main_window_id: None, awake: status.awake, tracking: status.ai_mode, tracking_speed: status.speed, @@ -60,7 +63,7 @@ impl MainPanel { text_input: String::new(), text_input_02: String::new(), }, - Task::none(), + window::get_latest().map(Message::ChangeMainWindowId), ) } @@ -79,6 +82,10 @@ impl MainPanel { let camera = self.camera.as_ref().unwrap(); match message { + Message::ChangeMainWindowId(id) => { + self.main_window_id = id; + Task::none() + } Message::ChangeSleeping(should_sleep) => { if should_sleep { self.awake = SleepMode::Sleep; From 9c3ca45e9c06998b3ffa8ec72406aa548bf4aeb9 Mon Sep 17 00:00:00 2001 From: Bono Fox Date: Thu, 30 Oct 2025 15:59:32 +0100 Subject: [PATCH 07/10] =?UTF-8?q?=E2=9C=A8=20Rebuild=20the=20window=20on?= =?UTF-8?q?=20window=20mode=20change?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - window mode can be changed - the current window mode is saved and readable in the view generation - on window mode change the old window is closed, the new one is built --- src/gui/main.rs | 470 ++++++++++++++++++++++++++---------------------- 1 file changed, 255 insertions(+), 215 deletions(-) diff --git a/src/gui/main.rs b/src/gui/main.rs index 9aabbda..9002963 100644 --- a/src/gui/main.rs +++ b/src/gui/main.rs @@ -3,16 +3,21 @@ mod styles; use crate::styles::theme::obsbot_theme; use iced::alignment::{Horizontal, Vertical}; use iced::widget::button::{primary, secondary}; -use iced::widget::{button, column, image, row, text, text_input, toggler}; +use iced::widget::{ + Container, Space, button, column, container, image, row, text, text_input, toggler, +}; use iced::window::Position; use iced::{Alignment, Length, Padding, Size, Subscription, Task, time, window}; use iced::{Element, Point}; +use iced_font_awesome::fa_icon_solid; use std::time::Duration; use tiny4linux::{AIMode, Camera, ExposureMode, OBSBotWebCam, SleepMode, TrackingSpeed}; use tiny4linux_assets::handle_t4l_asset; #[derive(Debug, Clone, PartialEq)] enum Message { + RequestWindowModeChange(WindowMode), + ApplyWindowMode(WindowMode), ChangeMainWindowId(Option), ChangeSleeping(bool), ChangeTracking(AIMode), @@ -33,6 +38,7 @@ enum Message { struct MainPanel { camera: Option, main_window_id: Option, + window_mode: WindowMode, awake: SleepMode, tracking: AIMode, tracking_speed: TrackingSpeed, @@ -43,7 +49,7 @@ struct MainPanel { } impl MainPanel { - fn init_state() -> (Self, Task) { + fn init_state(window_mode: WindowMode) -> (Self, Task) { let camera = Camera::new("OBSBOT Tiny 2").ok(); let status = camera @@ -55,6 +61,7 @@ impl MainPanel { MainPanel { camera, main_window_id: None, + window_mode, awake: status.awake, tracking: status.ai_mode, tracking_speed: status.speed, @@ -82,6 +89,24 @@ impl MainPanel { let camera = self.camera.as_ref().unwrap(); match message { + Message::RequestWindowModeChange(new_mode) => { + let close_task = self + .main_window_id + .map(|main_window_id| window::close::(main_window_id)) + .unwrap_or_else(Task::none); + + let (new_id, open_task) = + window::open(get_window_settings_for_window_mode(new_mode)); + let open_task = open_task.map(move |_| Message::ChangeMainWindowId(Some(new_id))); + + let apply_task = Task::done(Message::ApplyWindowMode(new_mode)); + + Task::batch([close_task, open_task, apply_task]) + } + Message::ApplyWindowMode(new_mode) => { + self.window_mode = new_mode; + Task::none() + } Message::ChangeMainWindowId(id) => { self.main_window_id = id; Task::none() @@ -161,218 +186,7 @@ impl MainPanel { fn view(&'_ self) -> Element<'_, Message> { if self.camera.is_some() { - let mut elements = column![ - row![ - text("Tiny4Linux") - .size(26) - .height(100) - .align_y(Vertical::Center), - image(if self.awake == SleepMode::Awake { - handle_t4l_asset("generated/png/icons/inverted-camera.png") - } else { - handle_t4l_asset("generated/png/icons/inverted-camera-asleep.png") - }) - .height(50) - ] - .spacing(30) - .align_y(Vertical::Center), - toggler(self.awake != SleepMode::Awake) - .label("Sleeping".to_string()) - .on_toggle(Message::ChangeSleeping), - button("Static") - .on_press(Message::ChangeTracking(AIMode::NoTracking)) - .style(if self.tracking == AIMode::NoTracking { - primary - } else { - secondary - }), - button("Normal Tracking") - .on_press(Message::ChangeTracking(AIMode::NormalTracking)) - .style(if self.tracking == AIMode::NormalTracking { - primary - } else { - secondary - }), - row![ - button("Upper Body") - .on_press(Message::ChangeTracking(AIMode::UpperBody)) - .width(Length::Fill) - .style(if self.tracking == AIMode::UpperBody { - primary - } else { - secondary - }), - button("Close-up") - .on_press(Message::ChangeTracking(AIMode::CloseUp)) - .width(Length::Fill) - .style(if self.tracking == AIMode::CloseUp { - primary - } else { - secondary - }), - ] - .spacing(10), - row![ - button("Headless") - .on_press(Message::ChangeTracking(AIMode::Headless)) - .width(Length::Fill) - .style(if self.tracking == AIMode::Headless { - primary - } else { - secondary - }), - button("Lower Body") - .on_press(Message::ChangeTracking(AIMode::LowerBody)) - .width(Length::Fill) - .style(if self.tracking == AIMode::LowerBody { - primary - } else { - secondary - }), - ] - .spacing(10), - row![ - button("Desk") - .on_press(Message::ChangeTracking(AIMode::DeskMode)) - .width(Length::Fill) - .style(if self.tracking == AIMode::DeskMode { - primary - } else { - secondary - }), - button("Whiteboard") - .on_press(Message::ChangeTracking(AIMode::Whiteboard)) - .width(Length::Fill) - .style(if self.tracking == AIMode::Whiteboard { - primary - } else { - secondary - }), - ] - .spacing(10), - row![ - button("Hand") - .on_press(Message::ChangeTracking(AIMode::Hand)) - .width(Length::Fill) - .style(if self.tracking == AIMode::Hand { - primary - } else { - secondary - }), - button("Group") - .on_press(Message::ChangeTracking(AIMode::Group)) - .width(Length::Fill) - .style(if self.tracking == AIMode::Group { - primary - } else { - secondary - }), - ] - .spacing(10), - column![ - text("Exposure: "), - row![ - button("Manual") - .on_press(Message::ChangeExposure(ExposureMode::Manual)) - .width(Length::Fill) - .style(secondary), - button("Face") - .on_press(Message::ChangeExposure(ExposureMode::Face)) - .width(Length::Fill) - .style(secondary), - button("Global") - .on_press(Message::ChangeExposure(ExposureMode::Global)) - .width(Length::Fill) - .style(secondary), - ] - .spacing(10) - ] - .spacing(10) - .padding(Padding::from([10, 0])), - column![ - text("Presets: "), - row![ - button("1") - .on_press(Message::ChangePresetPosition(0)) - .width(Length::Fill) - .style(secondary), - button("2") - .on_press(Message::ChangePresetPosition(1)) - .width(Length::Fill) - .style(secondary), - button("3") - .on_press(Message::ChangePresetPosition(2)) - .width(Length::Fill) - .style(secondary), - ] - .spacing(10) - ] - .spacing(10) - .padding(Padding::from([10, 0])), - column![ - text("Tracking Speed: "), - row![ - button("Standard") - .on_press(Message::ChangeTrackingSpeed(TrackingSpeed::Standard)) - .style(if self.tracking_speed == TrackingSpeed::Standard { - primary - } else { - secondary - }), - button("Sport") - .on_press(Message::ChangeTrackingSpeed(TrackingSpeed::Sport)) - .style(if self.tracking_speed == TrackingSpeed::Sport { - primary - } else { - secondary - }), - ] - .spacing(10) - ] - .spacing(10) - .padding(Padding::from([10, 0])), - toggler(self.hdr_on) - .label("HDR".to_string()) - .on_toggle(Message::ChangeHDR), - toggler(self.debugging_on) - .label("Debugging".to_string()) - .on_toggle(Message::ChangeDebugging), - text(if self.awake == SleepMode::Awake { - format!( - "{tracking_mode} ({tracking_speed})", - tracking_mode = self.tracking.to_string(), - tracking_speed = self.tracking_speed.to_string() - ) - } else { - self.awake.to_string() - }) - ]; - - if self.debugging_on { - elements = elements.push(column![ - text_input("0x06 hex string", &self.text_input) - .on_input(Message::TextInput) - .on_submit(Message::SendCommand), - text_input("0x02 hex string", &self.text_input_02) - .on_input(Message::TextInput02) - .on_submit(Message::SendCommand02), - button("Dump 0x06") - .on_press(Message::HexDump) - .width(Length::Fill), - button("Dump 0x02") - .on_press(Message::HexDump02) - .width(Length::Fill), - ]); - } - - let c = elements - .width(Length::Fill) - .height(Length::Fill) - .align_x(Alignment::Center) - .spacing(10) - .padding(10) - .into(); - c + get_current_ui_elements(self).into() } else { text("Camera could not be found. Please check the connection of the camera.") .size(20) @@ -450,6 +264,232 @@ fn get_start_mode() -> WindowMode { WindowMode::Dashboard } +fn get_current_ui_elements(app: &MainPanel) -> Container<'static, Message> { + let mut elements = column![ + match app.window_mode { + WindowMode::Dashboard => container( + button(fa_icon_solid("down-left-and-up-right-to-center")) + .on_press(Message::RequestWindowModeChange(WindowMode::Widget)), + ), + WindowMode::Widget => container( + button(fa_icon_solid("up-right-and-down-left-from-center")) + .on_press(Message::RequestWindowModeChange(WindowMode::Dashboard)), + ), + WindowMode::Invalid => container(Space::new(0, 0)), + }, + row![ + text("Tiny4Linux") + .size(26) + .height(100) + .align_y(Vertical::Center), + image(if app.awake == SleepMode::Awake { + handle_t4l_asset("generated/png/icons/inverted-camera.png") + } else { + handle_t4l_asset("generated/png/icons/inverted-camera-asleep.png") + }) + .height(50) + ] + .spacing(30) + .align_y(Vertical::Center), + toggler(app.awake != SleepMode::Awake) + .label("Sleeping".to_string()) + .on_toggle(Message::ChangeSleeping), + button("Static") + .on_press(Message::ChangeTracking(AIMode::NoTracking)) + .style(if app.tracking == AIMode::NoTracking { + primary + } else { + secondary + }), + button("Normal Tracking") + .on_press(Message::ChangeTracking(AIMode::NormalTracking)) + .style(if app.tracking == AIMode::NormalTracking { + primary + } else { + secondary + }), + row![ + button("Upper Body") + .on_press(Message::ChangeTracking(AIMode::UpperBody)) + .width(Length::Fill) + .style(if app.tracking == AIMode::UpperBody { + primary + } else { + secondary + }), + button("Close-up") + .on_press(Message::ChangeTracking(AIMode::CloseUp)) + .width(Length::Fill) + .style(if app.tracking == AIMode::CloseUp { + primary + } else { + secondary + }), + ] + .spacing(10), + row![ + button("Headless") + .on_press(Message::ChangeTracking(AIMode::Headless)) + .width(Length::Fill) + .style(if app.tracking == AIMode::Headless { + primary + } else { + secondary + }), + button("Lower Body") + .on_press(Message::ChangeTracking(AIMode::LowerBody)) + .width(Length::Fill) + .style(if app.tracking == AIMode::LowerBody { + primary + } else { + secondary + }), + ] + .spacing(10), + row![ + button("Desk") + .on_press(Message::ChangeTracking(AIMode::DeskMode)) + .width(Length::Fill) + .style(if app.tracking == AIMode::DeskMode { + primary + } else { + secondary + }), + button("Whiteboard") + .on_press(Message::ChangeTracking(AIMode::Whiteboard)) + .width(Length::Fill) + .style(if app.tracking == AIMode::Whiteboard { + primary + } else { + secondary + }), + ] + .spacing(10), + row![ + button("Hand") + .on_press(Message::ChangeTracking(AIMode::Hand)) + .width(Length::Fill) + .style(if app.tracking == AIMode::Hand { + primary + } else { + secondary + }), + button("Group") + .on_press(Message::ChangeTracking(AIMode::Group)) + .width(Length::Fill) + .style(if app.tracking == AIMode::Group { + primary + } else { + secondary + }), + ] + .spacing(10), + column![ + text("Exposure: "), + row![ + button("Manual") + .on_press(Message::ChangeExposure(ExposureMode::Manual)) + .width(Length::Fill) + .style(secondary), + button("Face") + .on_press(Message::ChangeExposure(ExposureMode::Face)) + .width(Length::Fill) + .style(secondary), + button("Global") + .on_press(Message::ChangeExposure(ExposureMode::Global)) + .width(Length::Fill) + .style(secondary), + ] + .spacing(10) + ] + .spacing(10) + .padding(Padding::from([10, 0])), + column![ + text("Presets: "), + row![ + button("1") + .on_press(Message::ChangePresetPosition(0)) + .width(Length::Fill) + .style(secondary), + button("2") + .on_press(Message::ChangePresetPosition(1)) + .width(Length::Fill) + .style(secondary), + button("3") + .on_press(Message::ChangePresetPosition(2)) + .width(Length::Fill) + .style(secondary), + ] + .spacing(10) + ] + .spacing(10) + .padding(Padding::from([10, 0])), + column![ + text("Tracking Speed: "), + row![ + button("Standard") + .on_press(Message::ChangeTrackingSpeed(TrackingSpeed::Standard)) + .style(if app.tracking_speed == TrackingSpeed::Standard { + primary + } else { + secondary + }), + button("Sport") + .on_press(Message::ChangeTrackingSpeed(TrackingSpeed::Sport)) + .style(if app.tracking_speed == TrackingSpeed::Sport { + primary + } else { + secondary + }), + ] + .spacing(10) + ] + .spacing(10) + .padding(Padding::from([10, 0])), + toggler(app.hdr_on) + .label("HDR".to_string()) + .on_toggle(Message::ChangeHDR), + toggler(app.debugging_on) + .label("Debugging".to_string()) + .on_toggle(Message::ChangeDebugging), + text(if app.awake == SleepMode::Awake { + format!( + "{tracking_mode} ({tracking_speed})", + tracking_mode = app.tracking.to_string(), + tracking_speed = app.tracking_speed.to_string() + ) + } else { + app.awake.to_string() + }) + ]; + + if app.debugging_on { + elements = elements.push(column![ + text_input("0x06 hex string", &app.text_input) + .on_input(Message::TextInput) + .on_submit(Message::SendCommand), + text_input("0x02 hex string", &app.text_input_02) + .on_input(Message::TextInput02) + .on_submit(Message::SendCommand02), + button("Dump 0x06") + .on_press(Message::HexDump) + .width(Length::Fill), + button("Dump 0x02") + .on_press(Message::HexDump02) + .width(Length::Fill), + ]); + } + + container( + elements + .width(Length::Fill) + .height(Length::Fill) + .align_x(Alignment::Center) + .spacing(10) + .padding(10), + ) +} + fn main() -> iced::Result { let start_mode = get_start_mode(); @@ -466,5 +506,5 @@ fn main() -> iced::Result { .theme(|_| obsbot_theme()) .window(get_window_settings_for_window_mode(start_mode)) .subscription(MainPanel::subscription) - .run_with(move || MainPanel::init_state()) + .run_with(move || MainPanel::init_state(start_mode)) } From 6d279d09c040ddca0f1daf3a299a6aeedcf67f88 Mon Sep 17 00:00:00 2001 From: Bono Fox Date: Thu, 30 Oct 2025 16:01:32 +0100 Subject: [PATCH 08/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Extracted=20colors?= =?UTF-8?q?=20in=20constants=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/styles/colors.rs | 4 ++++ src/gui/styles/mod.rs | 1 + src/gui/styles/theme.rs | 7 ++++--- 3 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 src/gui/styles/colors.rs diff --git a/src/gui/styles/colors.rs b/src/gui/styles/colors.rs new file mode 100644 index 0000000..3219802 --- /dev/null +++ b/src/gui/styles/colors.rs @@ -0,0 +1,4 @@ +use iced::{Color, color}; + +pub const COLOR_PRIMARY_OBSBOT: Color = color!(0xe6, 0x00, 0x33); +pub const COLOR_BACKGROUND_DARK: Color = color!(0x19, 0x1a, 0x1b); diff --git a/src/gui/styles/mod.rs b/src/gui/styles/mod.rs index 9757ba5..5915117 100644 --- a/src/gui/styles/mod.rs +++ b/src/gui/styles/mod.rs @@ -1 +1,2 @@ +pub mod colors; pub mod theme; diff --git a/src/gui/styles/theme.rs b/src/gui/styles/theme.rs index 4118e97..de9e753 100644 --- a/src/gui/styles/theme.rs +++ b/src/gui/styles/theme.rs @@ -1,11 +1,12 @@ +use crate::styles::colors::{COLOR_BACKGROUND_DARK, COLOR_PRIMARY_OBSBOT}; +use iced::Theme; use iced::theme::Palette; use iced::theme::palette::Extended; -use iced::{Color, Theme}; pub fn obsbot_theme() -> Theme { let palette = Palette { - primary: Color::from_rgb8(0xe6, 0x00, 0x33), - background: Color::from_rgb8(0x19, 0x1a, 0x1b), + primary: COLOR_PRIMARY_OBSBOT, + background: COLOR_BACKGROUND_DARK, success: Default::default(), text: Default::default(), danger: Default::default(), From 1ecbb014c16aff1e2a06f34271895a0f62aff02f Mon Sep 17 00:00:00 2001 From: Bono Fox Date: Thu, 30 Oct 2025 16:05:57 +0100 Subject: [PATCH 09/10] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Added=20Display=20na?= =?UTF-8?q?mes=20for=20exposure=20modes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 657c865..863faed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -185,6 +185,16 @@ pub enum ExposureMode { Face, } +impl Display for ExposureMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ExposureMode::Manual => write!(f, "Manual"), + ExposureMode::Global => write!(f, "Global"), + ExposureMode::Face => write!(f, "Face"), + } + } +} + #[derive(Debug, Clone, Copy, PartialEq)] pub enum ExposureModeType { Auto, From e057204c7a369a7a4446e123925229ffb3e6b5d2 Mon Sep 17 00:00:00 2001 From: Bono Fox Date: Thu, 30 Oct 2025 16:10:18 +0100 Subject: [PATCH 10/10] =?UTF-8?q?=F0=9F=92=A5=20Complete=20GUI-rework?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - extracted all UI-generation out of main-file into module `ui_modules` - dynamic content according to window mode - rethought all interactions - generally only buttons, no other controls, for simplicity & uniformity - added readonly stats-area in dashboard - added tooltips for buttons - added colored areas for better UX --- src/gui/main.rs | 235 +----------------- src/gui/styles/button_non_styled.rs | 9 + src/gui/styles/colors.rs | 1 + src/gui/styles/general_area_style.rs | 9 + src/gui/styles/mod.rs | 3 + src/gui/styles/tooltip_style.rs | 21 ++ src/gui/ui_modules/button_exposure_mode.rs | 19 ++ src/gui/ui_modules/button_hdr.rs | 26 ++ src/gui/ui_modules/button_sleep_wake.rs | 42 ++++ src/gui/ui_modules/button_tracking_mode.rs | 43 ++++ .../ui_modules/button_window_mode_change.rs | 23 ++ src/gui/ui_modules/current_stats.rs | 107 ++++++++ src/gui/ui_modules/debug_area.rs | 80 ++++++ src/gui/ui_modules/mod.rs | 9 + src/gui/ui_modules/settings_area.rs | 145 +++++++++++ src/gui/ui_modules/window_layout.rs | 119 +++++++++ 16 files changed, 661 insertions(+), 230 deletions(-) create mode 100644 src/gui/styles/button_non_styled.rs create mode 100644 src/gui/styles/general_area_style.rs create mode 100644 src/gui/styles/tooltip_style.rs create mode 100644 src/gui/ui_modules/button_exposure_mode.rs create mode 100644 src/gui/ui_modules/button_hdr.rs create mode 100644 src/gui/ui_modules/button_sleep_wake.rs create mode 100644 src/gui/ui_modules/button_tracking_mode.rs create mode 100644 src/gui/ui_modules/button_window_mode_change.rs create mode 100644 src/gui/ui_modules/current_stats.rs create mode 100644 src/gui/ui_modules/debug_area.rs create mode 100644 src/gui/ui_modules/mod.rs create mode 100644 src/gui/ui_modules/settings_area.rs create mode 100644 src/gui/ui_modules/window_layout.rs diff --git a/src/gui/main.rs b/src/gui/main.rs index 9002963..9050a31 100644 --- a/src/gui/main.rs +++ b/src/gui/main.rs @@ -1,18 +1,15 @@ mod styles; +mod ui_modules; use crate::styles::theme::obsbot_theme; +use crate::ui_modules::window_layout::window_layout; use iced::alignment::{Horizontal, Vertical}; -use iced::widget::button::{primary, secondary}; -use iced::widget::{ - Container, Space, button, column, container, image, row, text, text_input, toggler, -}; +use iced::widget::{Container, text}; use iced::window::Position; -use iced::{Alignment, Length, Padding, Size, Subscription, Task, time, window}; use iced::{Element, Point}; -use iced_font_awesome::fa_icon_solid; +use iced::{Length, Size, Subscription, Task, time, window}; use std::time::Duration; use tiny4linux::{AIMode, Camera, ExposureMode, OBSBotWebCam, SleepMode, TrackingSpeed}; -use tiny4linux_assets::handle_t4l_asset; #[derive(Debug, Clone, PartialEq)] enum Message { @@ -265,229 +262,7 @@ fn get_start_mode() -> WindowMode { } fn get_current_ui_elements(app: &MainPanel) -> Container<'static, Message> { - let mut elements = column![ - match app.window_mode { - WindowMode::Dashboard => container( - button(fa_icon_solid("down-left-and-up-right-to-center")) - .on_press(Message::RequestWindowModeChange(WindowMode::Widget)), - ), - WindowMode::Widget => container( - button(fa_icon_solid("up-right-and-down-left-from-center")) - .on_press(Message::RequestWindowModeChange(WindowMode::Dashboard)), - ), - WindowMode::Invalid => container(Space::new(0, 0)), - }, - row![ - text("Tiny4Linux") - .size(26) - .height(100) - .align_y(Vertical::Center), - image(if app.awake == SleepMode::Awake { - handle_t4l_asset("generated/png/icons/inverted-camera.png") - } else { - handle_t4l_asset("generated/png/icons/inverted-camera-asleep.png") - }) - .height(50) - ] - .spacing(30) - .align_y(Vertical::Center), - toggler(app.awake != SleepMode::Awake) - .label("Sleeping".to_string()) - .on_toggle(Message::ChangeSleeping), - button("Static") - .on_press(Message::ChangeTracking(AIMode::NoTracking)) - .style(if app.tracking == AIMode::NoTracking { - primary - } else { - secondary - }), - button("Normal Tracking") - .on_press(Message::ChangeTracking(AIMode::NormalTracking)) - .style(if app.tracking == AIMode::NormalTracking { - primary - } else { - secondary - }), - row![ - button("Upper Body") - .on_press(Message::ChangeTracking(AIMode::UpperBody)) - .width(Length::Fill) - .style(if app.tracking == AIMode::UpperBody { - primary - } else { - secondary - }), - button("Close-up") - .on_press(Message::ChangeTracking(AIMode::CloseUp)) - .width(Length::Fill) - .style(if app.tracking == AIMode::CloseUp { - primary - } else { - secondary - }), - ] - .spacing(10), - row![ - button("Headless") - .on_press(Message::ChangeTracking(AIMode::Headless)) - .width(Length::Fill) - .style(if app.tracking == AIMode::Headless { - primary - } else { - secondary - }), - button("Lower Body") - .on_press(Message::ChangeTracking(AIMode::LowerBody)) - .width(Length::Fill) - .style(if app.tracking == AIMode::LowerBody { - primary - } else { - secondary - }), - ] - .spacing(10), - row![ - button("Desk") - .on_press(Message::ChangeTracking(AIMode::DeskMode)) - .width(Length::Fill) - .style(if app.tracking == AIMode::DeskMode { - primary - } else { - secondary - }), - button("Whiteboard") - .on_press(Message::ChangeTracking(AIMode::Whiteboard)) - .width(Length::Fill) - .style(if app.tracking == AIMode::Whiteboard { - primary - } else { - secondary - }), - ] - .spacing(10), - row![ - button("Hand") - .on_press(Message::ChangeTracking(AIMode::Hand)) - .width(Length::Fill) - .style(if app.tracking == AIMode::Hand { - primary - } else { - secondary - }), - button("Group") - .on_press(Message::ChangeTracking(AIMode::Group)) - .width(Length::Fill) - .style(if app.tracking == AIMode::Group { - primary - } else { - secondary - }), - ] - .spacing(10), - column![ - text("Exposure: "), - row![ - button("Manual") - .on_press(Message::ChangeExposure(ExposureMode::Manual)) - .width(Length::Fill) - .style(secondary), - button("Face") - .on_press(Message::ChangeExposure(ExposureMode::Face)) - .width(Length::Fill) - .style(secondary), - button("Global") - .on_press(Message::ChangeExposure(ExposureMode::Global)) - .width(Length::Fill) - .style(secondary), - ] - .spacing(10) - ] - .spacing(10) - .padding(Padding::from([10, 0])), - column![ - text("Presets: "), - row![ - button("1") - .on_press(Message::ChangePresetPosition(0)) - .width(Length::Fill) - .style(secondary), - button("2") - .on_press(Message::ChangePresetPosition(1)) - .width(Length::Fill) - .style(secondary), - button("3") - .on_press(Message::ChangePresetPosition(2)) - .width(Length::Fill) - .style(secondary), - ] - .spacing(10) - ] - .spacing(10) - .padding(Padding::from([10, 0])), - column![ - text("Tracking Speed: "), - row![ - button("Standard") - .on_press(Message::ChangeTrackingSpeed(TrackingSpeed::Standard)) - .style(if app.tracking_speed == TrackingSpeed::Standard { - primary - } else { - secondary - }), - button("Sport") - .on_press(Message::ChangeTrackingSpeed(TrackingSpeed::Sport)) - .style(if app.tracking_speed == TrackingSpeed::Sport { - primary - } else { - secondary - }), - ] - .spacing(10) - ] - .spacing(10) - .padding(Padding::from([10, 0])), - toggler(app.hdr_on) - .label("HDR".to_string()) - .on_toggle(Message::ChangeHDR), - toggler(app.debugging_on) - .label("Debugging".to_string()) - .on_toggle(Message::ChangeDebugging), - text(if app.awake == SleepMode::Awake { - format!( - "{tracking_mode} ({tracking_speed})", - tracking_mode = app.tracking.to_string(), - tracking_speed = app.tracking_speed.to_string() - ) - } else { - app.awake.to_string() - }) - ]; - - if app.debugging_on { - elements = elements.push(column![ - text_input("0x06 hex string", &app.text_input) - .on_input(Message::TextInput) - .on_submit(Message::SendCommand), - text_input("0x02 hex string", &app.text_input_02) - .on_input(Message::TextInput02) - .on_submit(Message::SendCommand02), - button("Dump 0x06") - .on_press(Message::HexDump) - .width(Length::Fill), - button("Dump 0x02") - .on_press(Message::HexDump02) - .width(Length::Fill), - ]); - } - - container( - elements - .width(Length::Fill) - .height(Length::Fill) - .align_x(Alignment::Center) - .spacing(10) - .padding(10), - ) + window_layout(app).width(Length::Fill).height(Length::Fill) } fn main() -> iced::Result { diff --git a/src/gui/styles/button_non_styled.rs b/src/gui/styles/button_non_styled.rs new file mode 100644 index 0000000..d538a1c --- /dev/null +++ b/src/gui/styles/button_non_styled.rs @@ -0,0 +1,9 @@ +use iced::Theme; +use iced::widget::button::{Status, Style}; + +pub fn button_non_styled(_: &Theme, _: Status) -> Style { + Style { + background: None, + ..Default::default() + } +} diff --git a/src/gui/styles/colors.rs b/src/gui/styles/colors.rs index 3219802..a3eb860 100644 --- a/src/gui/styles/colors.rs +++ b/src/gui/styles/colors.rs @@ -2,3 +2,4 @@ use iced::{Color, color}; pub const COLOR_PRIMARY_OBSBOT: Color = color!(0xe6, 0x00, 0x33); pub const COLOR_BACKGROUND_DARK: Color = color!(0x19, 0x1a, 0x1b); +pub const COLOR_BACKGROUND_SECONDARY_DARK: Color = color!(0x29, 0x2a, 0x2b); diff --git a/src/gui/styles/general_area_style.rs b/src/gui/styles/general_area_style.rs new file mode 100644 index 0000000..c69b6b6 --- /dev/null +++ b/src/gui/styles/general_area_style.rs @@ -0,0 +1,9 @@ +use crate::styles::colors::COLOR_BACKGROUND_SECONDARY_DARK; +use iced::widget::container; + +pub fn general_area_style() -> container::Style { + container::Style { + background: Some(COLOR_BACKGROUND_SECONDARY_DARK.into()), + ..Default::default() + } +} diff --git a/src/gui/styles/mod.rs b/src/gui/styles/mod.rs index 5915117..c2c88df 100644 --- a/src/gui/styles/mod.rs +++ b/src/gui/styles/mod.rs @@ -1,2 +1,5 @@ +pub mod button_non_styled; pub mod colors; +pub mod general_area_style; pub mod theme; +pub mod tooltip_style; diff --git a/src/gui/styles/tooltip_style.rs b/src/gui/styles/tooltip_style.rs new file mode 100644 index 0000000..6c3c011 --- /dev/null +++ b/src/gui/styles/tooltip_style.rs @@ -0,0 +1,21 @@ +use crate::Message; +use crate::styles::colors::{COLOR_BACKGROUND_DARK, COLOR_BACKGROUND_SECONDARY_DARK}; +use iced::Border; +use iced::border::radius; +use iced::widget::{Container, container}; + +pub fn tooltip_content(content: Container) -> Container { + content.padding(10).style(|_| tooltip_style()) +} + +fn tooltip_style() -> container::Style { + container::Style { + background: Some(COLOR_BACKGROUND_SECONDARY_DARK.into()), + border: Border { + color: COLOR_BACKGROUND_DARK, + width: 2.0, + radius: radius(0), + }, + ..Default::default() + } +} diff --git a/src/gui/ui_modules/button_exposure_mode.rs b/src/gui/ui_modules/button_exposure_mode.rs new file mode 100644 index 0000000..0335b80 --- /dev/null +++ b/src/gui/ui_modules/button_exposure_mode.rs @@ -0,0 +1,19 @@ +use crate::Message; +use crate::styles::tooltip_style::tooltip_content; +use iced::widget::button::secondary; +use iced::widget::tooltip::Position; +use iced::widget::{Container, button, container, text, tooltip}; +use tiny4linux::ExposureMode; + +pub fn button_exposure_mode(mode: ExposureMode) -> Container<'static, Message> { + container(tooltip( + button(text(format!("{}", mode))) + .on_press(Message::ChangeExposure(mode)) + .style(secondary), + tooltip_content(container(text(format!( + "Changes the exposure mode to {} exposure", + mode + )))), + Position::Bottom, + )) +} diff --git a/src/gui/ui_modules/button_hdr.rs b/src/gui/ui_modules/button_hdr.rs new file mode 100644 index 0000000..c37d82c --- /dev/null +++ b/src/gui/ui_modules/button_hdr.rs @@ -0,0 +1,26 @@ +use crate::Message; +use crate::styles::tooltip_style::tooltip_content; +use iced::alignment::Vertical; +use iced::widget::tooltip::Position; +use iced::widget::{Container, button, container, row, text, tooltip}; +use iced_font_awesome::fa_icon_solid; + +pub fn button_hdr(current_mode: bool) -> Container<'static, Message> { + container(tooltip( + button( + row![ + fa_icon_solid(if current_mode { "palette" } else { "power-off" }), + text(if current_mode { "HDR on" } else { "HDR off" }) + ] + .align_y(Vertical::Center) + .spacing(5), + ) + .on_press(Message::ChangeHDR(!current_mode)), + tooltip_content(container(if current_mode { + "Turns off HDR on click" + } else { + "Turns on HDR on click" + })), + Position::Bottom, + )) +} diff --git a/src/gui/ui_modules/button_sleep_wake.rs b/src/gui/ui_modules/button_sleep_wake.rs new file mode 100644 index 0000000..df2b454 --- /dev/null +++ b/src/gui/ui_modules/button_sleep_wake.rs @@ -0,0 +1,42 @@ +use crate::styles::tooltip_style::tooltip_content; +use crate::{Message, WindowMode}; +use iced::widget::tooltip::Position; +use iced::widget::{Container, button, container, row, tooltip}; +use iced_font_awesome::fa_icon_solid; +use tiny4linux::SleepMode; + +pub fn button_sleep_wake( + sleep_mode: SleepMode, + window_mode: WindowMode, +) -> Container<'static, Message> { + let (text_element_text, icon, tooltip_text, message) = match sleep_mode { + SleepMode::Awake => ( + "Set to Sleep", + fa_icon_solid("moon"), + "Request the camera to go to sleep mode", + Message::ChangeSleeping(true), + ), + SleepMode::Sleep => ( + "Wake Up", + fa_icon_solid("sun"), + "Request the camera to wake up from sleep mode", + Message::ChangeSleeping(false), + ), + SleepMode::Unknown => ( + "Sleep state unknown", + fa_icon_solid("question"), + "The state can't be determined. Click the button to try setting the mode or check the connection to the camera.", + Message::ChangeSleeping(true), + ), + }; + + container(tooltip( + button(row![icon, text_element_text].spacing(5)).on_press(message), + tooltip_content(container(tooltip_text)), + if window_mode == WindowMode::Widget { + Position::Bottom + } else { + Position::Right + }, + )) +} diff --git a/src/gui/ui_modules/button_tracking_mode.rs b/src/gui/ui_modules/button_tracking_mode.rs new file mode 100644 index 0000000..82b7399 --- /dev/null +++ b/src/gui/ui_modules/button_tracking_mode.rs @@ -0,0 +1,43 @@ +use crate::Message; +use crate::styles::tooltip_style::tooltip_content; +use iced::widget::button::{primary, secondary}; +use iced::widget::tooltip::Position; +use iced::widget::{Container, button, container, text, tooltip}; +use tiny4linux::{AIMode, TrackingSpeed}; + +pub fn button_tracking_mode(mode: AIMode, current_mode: AIMode) -> Container<'static, Message> { + container(tooltip( + button(text(format!("{}", mode.to_string()))) + .on_press(Message::ChangeTracking(mode)) + .style(if current_mode == mode { + primary + } else { + secondary + }), + tooltip_content(container(text(format!( + "Set the camera to {} tracking mode", + mode.to_string() + )))), + Position::Bottom, + )) +} + +pub fn button_tracking_speed( + speed: TrackingSpeed, + current_speed: TrackingSpeed, +) -> Container<'static, Message> { + container(tooltip( + button(text(format!("{}", speed.to_string()))) + .on_press(Message::ChangeTrackingSpeed(speed)) + .style(if current_speed == speed { + primary + } else { + secondary + }), + tooltip_content(container(text(format!( + "Set the camera's speed to {}", + speed.to_string() + )))), + Position::Bottom, + )) +} diff --git a/src/gui/ui_modules/button_window_mode_change.rs b/src/gui/ui_modules/button_window_mode_change.rs new file mode 100644 index 0000000..c2119cd --- /dev/null +++ b/src/gui/ui_modules/button_window_mode_change.rs @@ -0,0 +1,23 @@ +use crate::styles::tooltip_style::tooltip_content; +use crate::{Message, WindowMode}; +use iced::widget::tooltip::Position; +use iced::widget::{Container, Space, button, container, tooltip}; +use iced_font_awesome::fa_icon_solid; + +pub fn button_window_mode_change(window_mode: WindowMode) -> Container<'static, Message> { + match window_mode { + WindowMode::Dashboard => container(tooltip( + button(fa_icon_solid("down-left-and-up-right-to-center")) + .on_press(Message::RequestWindowModeChange(WindowMode::Widget)), + tooltip_content(container("Switch to Widget-Mode")), + Position::Bottom, + )), + WindowMode::Widget => container(tooltip( + button(fa_icon_solid("up-right-and-down-left-from-center")) + .on_press(Message::RequestWindowModeChange(WindowMode::Dashboard)), + tooltip_content(container("Switch to Dashboard-Mode")), + Position::Bottom, + )), + WindowMode::Invalid => container(Space::new(0, 0)), + } +} diff --git a/src/gui/ui_modules/current_stats.rs b/src/gui/ui_modules/current_stats.rs new file mode 100644 index 0000000..e1b437f --- /dev/null +++ b/src/gui/ui_modules/current_stats.rs @@ -0,0 +1,107 @@ +use crate::{MainPanel, Message}; +use iced::alignment::Horizontal; +use iced::widget::{Container, column, container, horizontal_rule, row, text}; +use iced_font_awesome::fa_icon_solid; +use tiny4linux::{AIMode, SleepMode, TrackingSpeed}; + +const TEXT_INDENT: &str = " "; +const TEXT_INDENT_LONG: &str = " "; + +pub fn current_stats(app: &MainPanel) -> Container<'static, Message> { + container( + column![ + text("Camera status:"), + column![ + row![ + text(TEXT_INDENT), + fa_icon_solid("moon"), + text("Sleep Mode:") + ] + .spacing(10), + row![ + text(TEXT_INDENT_LONG), + match app.awake { + SleepMode::Awake => row![fa_icon_solid("eye"), text("Awake")].spacing(10), + SleepMode::Sleep => + row![fa_icon_solid("eye-slash"), text("Sleeping")].spacing(10), + SleepMode::Unknown => + row![fa_icon_solid("question-circle"), text("Unknown")].spacing(10), + } + ] + .spacing(10), + horizontal_rule(1), + row![text(TEXT_INDENT), fa_icon_solid("robot"), text("AI Mode:"),].spacing(10), + row![ + text(TEXT_INDENT_LONG), + match app.tracking { + AIMode::NoTracking => { + row![fa_icon_solid("tape"), text("Static")].spacing(10) + } + AIMode::NormalTracking => { + row![fa_icon_solid("user"), text("Normal")].spacing(10) + } + AIMode::UpperBody => { + row![fa_icon_solid("user-plus"), text("Upper Body")].spacing(10) + } + AIMode::CloseUp => { + row![fa_icon_solid("face-smile"), text("Close Up")].spacing(10) + } + AIMode::Headless => { + row![fa_icon_solid("circle-xmark"), text("Headless Body")].spacing(10) + } + AIMode::LowerBody => { + row![fa_icon_solid("down-long"), text("Lower Body")].spacing(10) + } + AIMode::DeskMode => { + row![fa_icon_solid("stapler"), text("Desk")].spacing(10) + } + AIMode::Whiteboard => { + row![fa_icon_solid("chalkboard"), text("Whiteboard")].spacing(10) + } + AIMode::Hand => { + row![fa_icon_solid("hand"), text("Hand Tracking")].spacing(10) + } + AIMode::Group => { + row![fa_icon_solid("users-viewfinder"), text("Group")].spacing(10) + } + AIMode::Unknown => { + row![fa_icon_solid("question-circle"), text("Unknown")].spacing(10) + } + } + ] + .spacing(10), + horizontal_rule(1), + row![ + text(TEXT_INDENT), + fa_icon_solid("gauge"), + text("Tracking Speed:"), + ] + .spacing(10), + row![ + text(TEXT_INDENT_LONG), + match app.tracking_speed { + TrackingSpeed::Standard => + row![fa_icon_solid("gauge-simple"), text("Standard")].spacing(10), + TrackingSpeed::Sport => + row![fa_icon_solid("gauge-simple-high"), text("Sport")].spacing(10), + } + ] + .spacing(10), + horizontal_rule(1), + row![text(TEXT_INDENT), fa_icon_solid("palette"), text("HDR:"),].spacing(10), + row![ + text(TEXT_INDENT_LONG), + if app.hdr_on { + row![fa_icon_solid("toggle-on"), text("On")].spacing(10) + } else { + row![fa_icon_solid("toggle-off"), text("Off")].spacing(10) + } + ] + .spacing(10) + ] + .spacing(10) + ] + .spacing(15) + .align_x(Horizontal::Left), + ) +} diff --git a/src/gui/ui_modules/debug_area.rs b/src/gui/ui_modules/debug_area.rs new file mode 100644 index 0000000..4740681 --- /dev/null +++ b/src/gui/ui_modules/debug_area.rs @@ -0,0 +1,80 @@ +use crate::Message::{HexDump, HexDump02}; +use crate::{MainPanel, Message}; +use iced::Length; +use iced::alignment::Horizontal; +use iced::widget::button::{primary, secondary}; +use iced::widget::{Container, button, column, container, horizontal_space, row, text, text_input}; +use iced_font_awesome::fa_icon_solid; + +const DEBUG_BUTTON_WIDTH: f32 = 100.0; +const DEBUG_INPUT_WIDTH: f32 = 250.0; + +pub fn debug_area(app: &MainPanel) -> Container<'static, Message> { + let debugging_active = app.debugging_on; + + container( + column![ + button(if debugging_active { + row![fa_icon_solid("bug"), text("Turn off debugging")].spacing(5) + } else { + row![fa_icon_solid("bug-slash"), text("Turn on debugging")].spacing(5) + }) + .on_press(Message::ChangeDebugging(!debugging_active)) + .style(if debugging_active { primary } else { secondary }), + if debugging_active { + column![ + row![ + row![fa_icon_solid("paper-plane"), text("Send")].spacing(5), + horizontal_space(), + column![ + row![ + text_input("0x06 hex string", &app.text_input) + .on_input(Message::TextInput) + .on_submit(Message::SendCommand) + .width(DEBUG_INPUT_WIDTH), + button("Send 0x06") + .on_press(Message::SendCommand) + .width(DEBUG_BUTTON_WIDTH), + button("Clear") + .on_press(Message::TextInput("".parse().unwrap())) + .width(DEBUG_BUTTON_WIDTH) + ] + .spacing(15), + row![ + text_input("0x02 hex string", &app.text_input_02) + .on_input(Message::TextInput02) + .on_submit(Message::SendCommand02) + .width(DEBUG_INPUT_WIDTH), + button("Send 0x02") + .on_press(Message::SendCommand02) + .width(DEBUG_BUTTON_WIDTH), + button("Clear") + .on_press(Message::TextInput02("".parse().unwrap())) + .width(DEBUG_BUTTON_WIDTH) + ] + .spacing(15), + ] + .spacing(15), + ], + row![ + row![fa_icon_solid("satellite-dish"), text("Get & Dump")].spacing(5), + horizontal_space(), + button("0x06 hex") + .width(DEBUG_BUTTON_WIDTH) + .on_press(HexDump), + button("0x02 hex") + .width(DEBUG_BUTTON_WIDTH) + .on_press(HexDump02), + ] + .spacing(15) + ] + .spacing(10) + } else { + column![] + } + ] + .spacing(10) + .align_x(Horizontal::Center) + .width(Length::Fill), + ) +} diff --git a/src/gui/ui_modules/mod.rs b/src/gui/ui_modules/mod.rs new file mode 100644 index 0000000..16779ec --- /dev/null +++ b/src/gui/ui_modules/mod.rs @@ -0,0 +1,9 @@ +mod button_exposure_mode; +mod button_hdr; +mod button_sleep_wake; +mod button_tracking_mode; +mod button_window_mode_change; +mod current_stats; +mod debug_area; +mod settings_area; +pub mod window_layout; diff --git a/src/gui/ui_modules/settings_area.rs b/src/gui/ui_modules/settings_area.rs new file mode 100644 index 0000000..addb6fb --- /dev/null +++ b/src/gui/ui_modules/settings_area.rs @@ -0,0 +1,145 @@ +use crate::styles::tooltip_style::tooltip_content; +use crate::ui_modules::button_exposure_mode::button_exposure_mode; +use crate::ui_modules::button_hdr::button_hdr; +use crate::ui_modules::button_tracking_mode::{button_tracking_mode, button_tracking_speed}; +use crate::{MainPanel, Message, WindowMode}; +use iced::Length; +use iced::alignment::{Horizontal, Vertical}; +use iced::widget::tooltip::Position; +use iced::widget::{ + Container, Row, button, column, container, horizontal_rule, horizontal_space, row, text, + tooltip, +}; +use iced_font_awesome::fa_icon_solid; +use tiny4linux::{AIMode, ExposureMode, TrackingSpeed}; + +pub fn settings_area(app: &MainPanel) -> Container<'static, Message> { + container( + column![ + presets(), + horizontal_rule(8), + tracking_modes(app.window_mode == WindowMode::Widget, app.tracking), + tracking_speed(app.tracking_speed), + horizontal_rule(8), + row![hdr(app.hdr_on), horizontal_space(), exposure_mode()].align_y(Vertical::Center), + ] + .spacing(20), + ) + .padding(10) +} + +fn presets() -> Row<'static, Message> { + row![ + text("Presets:"), + horizontal_space().width(Length::FillPortion(2)), + (0..=2) + .fold(row![], |r, n| { + let r = r.push(tooltip( + button(fa_icon_solid(&(n + 1).to_string())) + .on_press(Message::ChangePresetPosition(n)), + tooltip_content(container(text(format!( + "Sets the cameras position to preset {}", + n + 1 + )))), + Position::Bottom, + )); + r.push(horizontal_space().width(Length::FillPortion(1))) + }) + .width(Length::FillPortion(6)), + horizontal_space().width(Length::FillPortion(2)) + ] +} + +fn tracking_modes(reduced: bool, current_mode: AIMode) -> Container<'static, Message> { + container( + column![ + text("Tracking:"), + if reduced { + column![ + row![ + button_tracking_mode(AIMode::NoTracking, current_mode), + button_tracking_mode(AIMode::NormalTracking, current_mode), + ] + .spacing(10), + row![ + button_tracking_mode(AIMode::Hand, current_mode), + button_tracking_mode(AIMode::Whiteboard, current_mode), + button_tracking_mode(AIMode::Group, current_mode), + ] + .spacing(10) + ] + .spacing(10) + .align_x(Horizontal::Center) + } else { + column![ + row![ + button_tracking_mode(AIMode::NoTracking, current_mode), + button_tracking_mode(AIMode::NormalTracking, current_mode), + ] + .spacing(10), + row![ + button_tracking_mode(AIMode::CloseUp, current_mode), + button_tracking_mode(AIMode::UpperBody, current_mode), + button_tracking_mode(AIMode::Headless, current_mode), + button_tracking_mode(AIMode::LowerBody, current_mode), + ] + .spacing(10), + row![ + button_tracking_mode(AIMode::DeskMode, current_mode), + button_tracking_mode(AIMode::Whiteboard, current_mode), + button_tracking_mode(AIMode::Hand, current_mode), + button_tracking_mode(AIMode::Group, current_mode), + ] + .spacing(10) + ] + .spacing(10) + .width(Length::Fill) + .align_x(Horizontal::Center) + } + ] + .width(Length::Fill) + .spacing(10), + ) +} + +fn tracking_speed(current_speed: TrackingSpeed) -> Container<'static, Message> { + container( + column![ + text("Tracking Speed:"), + column![ + row![ + button_tracking_speed(TrackingSpeed::Standard, current_speed), + button_tracking_speed(TrackingSpeed::Sport, current_speed), + ] + .spacing(10), + ] + .align_x(Horizontal::Center) + .width(Length::Fill) + ] + .spacing(10) + .width(Length::Fill), + ) +} + +fn hdr(current_mode: bool) -> Container<'static, Message> { + container( + column![text("HDR:"), button_hdr(current_mode)] + .spacing(5) + .align_x(Horizontal::Center) + .width(Length::Fill), + ) +} + +fn exposure_mode() -> Container<'static, Message> { + container( + column![ + text("Exposure:"), + button_exposure_mode(ExposureMode::Manual), + button_exposure_mode(ExposureMode::Global), + button_exposure_mode(ExposureMode::Face), + ] + .align_x(Horizontal::Center) + .width(Length::Fill) + .spacing(5), + ) +} diff --git a/src/gui/ui_modules/window_layout.rs b/src/gui/ui_modules/window_layout.rs new file mode 100644 index 0000000..9ef5238 --- /dev/null +++ b/src/gui/ui_modules/window_layout.rs @@ -0,0 +1,119 @@ +use crate::styles::button_non_styled::button_non_styled; +use crate::styles::general_area_style::general_area_style; +use crate::ui_modules::button_sleep_wake::button_sleep_wake; +use crate::ui_modules::button_window_mode_change::button_window_mode_change; +use crate::ui_modules::current_stats::current_stats; +use crate::ui_modules::debug_area::debug_area; +use crate::ui_modules::settings_area::settings_area; +use crate::{MainPanel, Message, WindowMode}; +use iced::alignment::Vertical; +use iced::widget::{ + Column, Container, Space, button, column, container, horizontal_rule, horizontal_space, image, + row, text, +}; +use iced::{Alignment, FillPortion, Length}; +use tiny4linux::SleepMode; +use tiny4linux_assets::handle_t4l_asset; + +pub fn window_layout(app: &MainPanel) -> Container<'static, Message> { + match app.window_mode { + WindowMode::Dashboard => container(row![ + dashboard_general_area(app).width(Length::FillPortion(1)), + dashboard_settings_area(app).width(Length::FillPortion(3)) + ]) + .width(Length::Fill) + .height(Length::Fill), + WindowMode::Widget => container(column![ + widget_head_area(app).height(Length::FillPortion(1)), + widget_body_area(app).height(Length::FillPortion(9)) + ]), + WindowMode::Invalid => container(Space::new(0, 0)), + } +} + +fn dashboard_general_area(app: &MainPanel) -> Container<'static, Message> { + container( + column![ + row![ + text("Tiny4Linux") + .size(18) + .height(100) + .align_y(Vertical::Center), + image(handle_t4l_asset( + "generated/png/title-icon/v2.0-soft-shadow.png" + )) + .height(60) + ] + .align_y(Alignment::Center) + .spacing(15), + text(match app.awake { + SleepMode::Awake => "The camera is awake", + SleepMode::Sleep => "The camera is sleeping", + SleepMode::Unknown => "The camera state is unknown", + }), + button(image(if app.awake == SleepMode::Awake { + handle_t4l_asset("generated/png/icons/inverted-camera.png") + } else { + handle_t4l_asset("generated/png/icons/inverted-camera-asleep.png") + })) + .on_press(if app.awake == SleepMode::Sleep { + Message::ChangeSleeping(false) + } else { + Message::ChangeSleeping(true) + }) + .height(100) + .style(button_non_styled), + button_sleep_wake(app.awake, WindowMode::Dashboard), + horizontal_rule(8), + row![ + horizontal_space().width(FillPortion(1)), + current_stats(app).width(FillPortion(8)), + horizontal_space().width(FillPortion(1)) + ] + ] + .align_x(Alignment::Center) + .width(Length::Fill) + .spacing(10) + .padding(5), + ) + .height(Length::Fill) + .style(|_| general_area_style()) +} + +fn dashboard_settings_area(app: &MainPanel) -> Column<'static, Message> { + column![ + row![ + Space::with_width(Length::Fill), + button_window_mode_change(WindowMode::Dashboard) + ], + settings_area(app), + debug_area(app) + ] +} + +fn widget_head_area(app: &MainPanel) -> Container<'static, Message> { + container( + row![ + button_window_mode_change(WindowMode::Widget), + row![ + image(if app.awake == SleepMode::Awake { + handle_t4l_asset("generated/png/icons/inverted-camera.png") + } else { + handle_t4l_asset("generated/png/icons/inverted-camera-asleep.png") + }) + .height(30), + button_sleep_wake(app.awake, WindowMode::Widget) + ] + .spacing(10) + ] + .width(Length::Fill) + .spacing(30), + ) + .padding(10) + .align_y(Vertical::Center) + .style(|_| general_area_style()) +} + +fn widget_body_area(app: &MainPanel) -> Column<'static, Message> { + column![settings_area(app)] +}