diff --git a/Cargo.lock b/Cargo.lock index 54bef4444..a3db6573b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2139,7 +2139,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad46a0e6c9bc688823a742aa969b5c08fdc56c2a436ee00d5c6fbcb5982c55c4" dependencies = [ - "libm", + "libm 0.2.8", ] [[package]] @@ -3493,6 +3493,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "libm" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" + [[package]] name = "libm" version = "0.2.8" @@ -3813,6 +3819,22 @@ dependencies = [ "tempfile", ] +[[package]] +name = "native-windows-gui" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f7003a669f68deb6b7c57d74fff4f8e533c44a3f0b297492440ef4ff5a28454" +dependencies = [ + "bitflags 1.3.2", + "lazy_static", + "newline-converter", + "plotters", + "plotters-backend", + "stretch", + "winapi 0.3.9", + "winapi-build", +] + [[package]] name = "ndk" version = "0.7.0" @@ -3891,6 +3913,15 @@ dependencies = [ "log", ] +[[package]] +name = "newline-converter" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f71d09d5c87634207f894c6b31b6a2b2c64ea3bdcf71bd5599fdbbe1600c00f" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "nix" version = "0.23.2" @@ -4574,6 +4605,24 @@ dependencies = [ "time 0.3.30", ] +[[package]] +name = "plotters" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" +dependencies = [ + "num-traits 0.2.17", + "plotters-backend", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" + [[package]] name = "png" version = "0.17.10" @@ -5390,6 +5439,7 @@ dependencies = [ "brotli", "dirs 5.0.1", "md5", + "native-windows-gui", "winapi 0.3.9", "winres", ] @@ -5948,6 +5998,16 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" +[[package]] +name = "stretch" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b0dc6d20ce137f302edf90f9cd3d278866fd7fb139efca6f246161222ad6d87" +dependencies = [ + "lazy_static", + "libm 0.1.4", +] + [[package]] name = "strsim" version = "0.8.0" diff --git a/libs/portable/Cargo.toml b/libs/portable/Cargo.toml index e39762a30..d0305b6b0 100644 --- a/libs/portable/Cargo.toml +++ b/libs/portable/Cargo.toml @@ -14,6 +14,9 @@ dirs = "5.0" md5 = "0.7" winapi = { version = "0.3", features = ["winbase"] } +[target.'cfg(target_os = "windows")'.dependencies] +native-windows-gui = "1.0" + [package.metadata.winres] LegalCopyright = "Copyright © 2024 Purslane Ltd. All rights reserved." ProductName = "RustDesk" diff --git a/libs/portable/src/main.rs b/libs/portable/src/main.rs index 1ffc8aa5d..48372d68b 100644 --- a/libs/portable/src/main.rs +++ b/libs/portable/src/main.rs @@ -8,6 +8,8 @@ use std::{ use bin_reader::BinaryReader; pub mod bin_reader; +#[cfg(windows)] +mod ui; #[cfg(windows)] const APP_METADATA: &[u8] = include_bytes!("../app_metadata.toml"); @@ -119,6 +121,11 @@ fn main() { let click_setup = args.is_empty() && arg_exe.to_lowercase().ends_with("install.exe"); let quick_support = args.is_empty() && arg_exe.to_lowercase().ends_with("qs.exe"); + #[cfg(windows)] + if args.is_empty() { + ui::setup(); + } + let reader = BinaryReader::default(); if let Some(exe) = setup( reader, diff --git a/libs/portable/src/res/label.png b/libs/portable/src/res/label.png new file mode 100644 index 000000000..6876c7935 Binary files /dev/null and b/libs/portable/src/res/label.png differ diff --git a/libs/portable/src/res/spin.gif b/libs/portable/src/res/spin.gif new file mode 100644 index 000000000..44b2e2e62 Binary files /dev/null and b/libs/portable/src/res/spin.gif differ diff --git a/libs/portable/src/ui.rs b/libs/portable/src/ui.rs new file mode 100644 index 000000000..0a015a248 --- /dev/null +++ b/libs/portable/src/ui.rs @@ -0,0 +1,232 @@ +use native_windows_gui as nwg; +use nwg::NativeUi; +use std::cell::RefCell; + +const GIF_DATA: &[u8] = include_bytes!("./res/spin.gif"); +const LABEL_DATA: &[u8] = include_bytes!("./res/label.png"); +const GIF_SIZE: i32 = 32; +const BG_COLOR: [u8; 3] = [90, 90, 120]; +const BORDER_COLOR: [u8; 3] = [40, 40, 40]; +const GIF_DELAY: u64 = 30; + +#[derive(Default)] +pub struct BasicApp { + window: nwg::Window, + + border_image: nwg::ImageFrame, + bg_image: nwg::ImageFrame, + gif_image: nwg::ImageFrame, + label_image: nwg::ImageFrame, + + border_layout: nwg::GridLayout, + bg_layout: nwg::GridLayout, + inner_layout: nwg::GridLayout, + + timer: nwg::AnimationTimer, + decoder: nwg::ImageDecoder, + gif_index: RefCell, + gif_images: RefCell>, +} + +impl BasicApp { + fn exit(&self) { + self.timer.stop(); + nwg::stop_thread_dispatch(); + } + + fn load_gif(&self) -> Result<(), nwg::NwgError> { + let image_source = self.decoder.from_stream(GIF_DATA)?; + for frame_index in 0..image_source.frame_count() { + let image_data = image_source.frame(frame_index)?; + let image_data = self + .decoder + .resize_image(&image_data, [GIF_SIZE as u32, GIF_SIZE as u32])?; + let bmp = image_data.as_bitmap()?; + self.gif_images.borrow_mut().push(bmp); + } + Ok(()) + } + + fn update_gif(&self) -> Result<(), nwg::NwgError> { + let images = self.gif_images.borrow(); + if images.len() == 0 { + return Err(nwg::NwgError::ImageDecoderError( + -1, + "no gif images".to_string(), + )); + } + let image_index = *self.gif_index.borrow() % images.len(); + self.gif_image.set_bitmap(Some(&images[image_index])); + *self.gif_index.borrow_mut() = (image_index + 1) % images.len(); + Ok(()) + } + + fn start_timer(&self) { + self.timer.start(); + } +} + +mod basic_app_ui { + use super::*; + use native_windows_gui::{self as nwg, Bitmap}; + use nwg::{Event, GridLayoutItem}; + use std::cell::RefCell; + use std::ops::Deref; + use std::rc::Rc; + + pub struct BasicAppUi { + inner: Rc, + default_handler: RefCell>, + } + + impl nwg::NativeUi for BasicApp { + fn build_ui(mut data: BasicApp) -> Result { + data.decoder = nwg::ImageDecoder::new()?; + let col_cnt: i32 = 7; + let row_cnt: i32 = 3; + let border_width: i32 = 1; + let window_size = ( + GIF_SIZE * col_cnt + 2 * border_width, + GIF_SIZE * row_cnt + 2 * border_width, + ); + + // Controls + nwg::Window::builder() + .flags(nwg::WindowFlags::POPUP | nwg::WindowFlags::VISIBLE) + .size(window_size) + .center(true) + .build(&mut data.window)?; + + nwg::ImageFrame::builder() + .parent(&data.window) + .size(window_size) + .background_color(Some(BORDER_COLOR)) + .build(&mut data.border_image)?; + + nwg::ImageFrame::builder() + .parent(&data.border_image) + .size((row_cnt * GIF_SIZE, col_cnt * GIF_SIZE)) + .background_color(Some(BG_COLOR)) + .build(&mut data.bg_image)?; + + nwg::ImageFrame::builder() + .parent(&data.bg_image) + .size((GIF_SIZE, GIF_SIZE)) + .background_color(Some(BG_COLOR)) + .build(&mut data.gif_image)?; + + nwg::ImageFrame::builder() + .parent(&data.bg_image) + .background_color(Some(BG_COLOR)) + .bitmap(Some(&Bitmap::from_bin(LABEL_DATA)?)) + .build(&mut data.label_image)?; + + nwg::AnimationTimer::builder() + .parent(&data.window) + .interval(std::time::Duration::from_millis(GIF_DELAY)) + .build(&mut data.timer)?; + + // Wrap-up + let ui = BasicAppUi { + inner: Rc::new(data), + default_handler: Default::default(), + }; + + // Layouts + nwg::GridLayout::builder() + .parent(&ui.window) + .spacing(0) + .margin([0, 0, 0, 0]) + .max_column(Some(1)) + .max_row(Some(1)) + .child_item(GridLayoutItem::new(&ui.border_image, 0, 0, 1, 1)) + .build(&ui.border_layout)?; + + nwg::GridLayout::builder() + .parent(&ui.border_image) + .spacing(0) + .margin([ + border_width as _, + border_width as _, + border_width as _, + border_width as _, + ]) + .max_column(Some(1)) + .max_row(Some(1)) + .child_item(GridLayoutItem::new(&ui.bg_image, 0, 0, 1, 1)) + .build(&ui.bg_layout)?; + + nwg::GridLayout::builder() + .parent(&ui.bg_image) + .spacing(0) + .margin([0, 0, 0, 0]) + .max_column(Some(col_cnt as _)) + .max_row(Some(row_cnt as _)) + .child_item(GridLayoutItem::new(&ui.gif_image, 2, 1, 1, 1)) + .child_item(GridLayoutItem::new(&ui.label_image, 3, 1, 3, 1)) + .build(&ui.inner_layout)?; + + // Events + let evt_ui = Rc::downgrade(&ui.inner); + let handle_events = move |evt, _evt_data, _handle| { + if let Some(evt_ui) = evt_ui.upgrade().as_mut() { + match evt { + Event::OnWindowClose => { + evt_ui.exit(); + } + Event::OnTimerTick => { + if let Err(e) = evt_ui.update_gif() { + eprintln!("{:?}", e); + } + } + _ => {} + } + } + }; + + ui.default_handler + .borrow_mut() + .push(nwg::full_bind_event_handler( + &ui.window.handle, + handle_events, + )); + + return Ok(ui); + } + } + + impl Drop for BasicAppUi { + /// To make sure that everything is freed without issues, the default handler must be unbound. + fn drop(&mut self) { + let mut handlers = self.default_handler.borrow_mut(); + for handler in handlers.drain(0..) { + nwg::unbind_event_handler(&handler); + } + } + } + + impl Deref for BasicAppUi { + type Target = BasicApp; + + fn deref(&self) -> &BasicApp { + &self.inner + } + } +} + +fn ui() -> Result<(), nwg::NwgError> { + nwg::init()?; + let app = BasicApp::build_ui(Default::default())?; + app.load_gif()?; + app.start_timer(); + nwg::dispatch_thread_events(); + Ok(()) +} + +pub fn setup() { + std::thread::spawn(move || { + if let Err(e) = ui() { + eprintln!("{:?}", e); + } + }); +}