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" 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/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/main.rs b/src/gui/main.rs index 97ef7a9..9050a31 100644 --- a/src/gui/main.rs +++ b/src/gui/main.rs @@ -1,17 +1,21 @@ mod styles; +mod ui_modules; use crate::styles::theme::obsbot_theme; -use iced::Element; +use crate::ui_modules::window_layout::window_layout; use iced::alignment::{Horizontal, Vertical}; -use iced::widget::button::{primary, secondary}; -use iced::widget::{button, column, image, row, text, text_input, toggler}; -use iced::{Alignment, Length, Padding, Size, Subscription, Task, time, window}; +use iced::widget::{Container, text}; +use iced::window::Position; +use iced::{Element, Point}; +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 { + RequestWindowModeChange(WindowMode), + ApplyWindowMode(WindowMode), + ChangeMainWindowId(Option), ChangeSleeping(bool), ChangeTracking(AIMode), ChangeTrackingSpeed(TrackingSpeed), @@ -30,6 +34,8 @@ enum Message { struct MainPanel { camera: Option, + main_window_id: Option, + window_mode: WindowMode, awake: SleepMode, tracking: AIMode, tracking_speed: TrackingSpeed, @@ -40,7 +46,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 @@ -51,6 +57,8 @@ impl MainPanel { ( MainPanel { camera, + main_window_id: None, + window_mode, awake: status.awake, tracking: status.ai_mode, tracking_speed: status.speed, @@ -59,7 +67,7 @@ impl MainPanel { text_input: String::new(), text_input_02: String::new(), }, - Task::none(), + window::get_latest().map(Message::ChangeMainWindowId), ) } @@ -78,6 +86,28 @@ 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() + } Message::ChangeSleeping(should_sleep) => { if should_sleep { self.awake = SleepMode::Sleep; @@ -153,218 +183,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) @@ -385,15 +204,82 @@ impl MainPanel { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum WindowMode { + Dashboard, + Widget, + 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(); + + 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") { + WindowMode::Dashboard + } else if start_mode_arg.eq_ignore_ascii_case("widget") { + WindowMode::Widget + } else { + WindowMode::Invalid + }; + } + } + + WindowMode::Dashboard +} + +fn get_current_ui_elements(app: &MainPanel) -> Container<'static, Message> { + window_layout(app).width(Length::Fill).height(Length::Fill) +} + fn main() -> iced::Result { + let start_mode = get_start_mode(); + + if start_mode == WindowMode::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); + iced::application("Tiny4Linux", MainPanel::update, MainPanel::view) .theme(|_| obsbot_theme()) - .window(window::Settings { - size: Size::new(300.0, 900.0), - 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(start_mode)) } 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 new file mode 100644 index 0000000..a3eb860 --- /dev/null +++ b/src/gui/styles/colors.rs @@ -0,0 +1,5 @@ +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 9757ba5..c2c88df 100644 --- a/src/gui/styles/mod.rs +++ b/src/gui/styles/mod.rs @@ -1 +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/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(), 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/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 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)] +} 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,