├── .gitignore ├── screenshot.png ├── Cargo.toml ├── README.md ├── LICENSE └── src ├── polyfill.js ├── webhack.rs ├── query.rs ├── gadget.rs └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/polyfloyd/gadgets-for-linux/HEAD/screenshot.png -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gadget-hack" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | async-std = "1.13.0" 8 | clap = { version = "4.5.17", features = ["cargo"], default-features = true } 9 | gtk4 = { version = "0.9", features = ["v4_12"], default-features = true } 10 | gtk4-layer-shell = "0.4.0" 11 | gtk4-sys = "0.9" 12 | html5ever = "0.27.0" 13 | markup5ever_rcdom = "0.3.0" 14 | serde_json = "1.0.128" 15 | sysinfo = "0.31.4" 16 | tempfile = "3.12.0" 17 | webkit6 = "0.4" 18 | xml5ever = "0.18.0" 19 | zip = "2.2.0" 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Gadgets for Linux 2 | ================= 3 | 4 | Software preservation effort for fun to make the original .gadget files from Windows Vista and 5 | Windows 7 work on Linux. 6 | 7 | Gadgets are a collection of web resources, hence this project uses WebKit embedded in GTK along 8 | with a bunch of polyfills to mimic the Microsoft API's. 9 | 10 | ![Screenshot](screenshot.png) 11 | 12 | Verified working gadgets: 13 | * MS Clock 14 | * MS CPU Gauge 15 | * AddGadgets All CPU Meter v1.3 (graph still missing) 16 | 17 | The gadget files are still subject to copyright as far as I am aware, so I am hesitant to include 18 | them here. However, they can be downloaded from 19 | [archive.org](http://web.archive.org/web/20111221105443/http://windows.microsoft.com/en-US/windows/downloads/personalize/gadgets) 20 | and [here](https://lelegofrog.github.io/wingadgets7.html) 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 polyfloyd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /src/polyfill.js: -------------------------------------------------------------------------------- 1 | 2 | let _style = document.createElement('style'); 3 | _style.type = 'text/css'; 4 | _style.innerHTML = ` 5 | textarea[disabled] { 6 | display: none; 7 | } 8 | `; 9 | document.getElementsByTagName('head')[0].appendChild(_style); 10 | 11 | 12 | class Settings { 13 | constructor() { 14 | this.values = {}; 15 | } 16 | 17 | read(key) { 18 | return this._values || null; 19 | } 20 | 21 | readString(key) { 22 | return this.read(key) || ''; 23 | } 24 | 25 | write(key, value) { 26 | this.values[key] = value; 27 | } 28 | } 29 | 30 | var System = { 31 | Gadget: { 32 | visible: true, 33 | path: 'dummy.gadget', 34 | Settings: new Settings(), 35 | }, 36 | Machine: null, // Set separately. 37 | Time: { 38 | currentTimeZone: 0, 39 | getLocalTime(timezone) { 40 | return Date.now() + timezone*60*1000; 41 | } 42 | }, 43 | }; 44 | 45 | 46 | Object.defineProperty(Array.prototype, 'count', { 47 | get() { return this.length; }, 48 | }); 49 | 50 | Array.prototype.item = function(i) { 51 | return this[i]; 52 | } 53 | 54 | 55 | // https://learn.microsoft.com/en-us/previous-versions/windows/desktop/sidebar/image-element 56 | Object.defineProperty(HTMLImageElement.prototype, 'Rotation', { 57 | set(r) { 58 | this.style.transform = `rotate(${r}deg)`; 59 | }, 60 | }); 61 | 62 | Object.defineProperty(HTMLImageElement.prototype, 'src', { 63 | set(s) { 64 | s = s.replace(/url\((.+)\)/, '$1'); 65 | this.setAttribute('src', s); 66 | }, 67 | }); 68 | 69 | // https://learn.microsoft.com/en-us/previous-versions/windows/desktop/sidebar/addshadow-method-gimage 70 | HTMLImageElement.prototype.addShadow = function(color, radius, alpha, dx, dy) { 71 | this.style.filter = `drop-shadow(${dx}px ${dy}px ${radius}px ${color})`; 72 | } 73 | 74 | // https://learn.microsoft.com/en-us/previous-versions/windows/desktop/sidebar/addglow-method-gimage 75 | HTMLImageElement.prototype.addGlow = function(color, radius, alpha) { 76 | this.style.filter = `drop-shadow(0 0 ${radius}px ${color})`; 77 | } 78 | 79 | 80 | class ActiveXObject { 81 | constructor() { 82 | this.values = {}; 83 | } 84 | 85 | RegRead(path) { 86 | return this._values || 'default'; 87 | } 88 | 89 | RegWrite(path, value) { 90 | this.values[path] = value; 91 | } 92 | 93 | FileExists(path) { 94 | return false; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/webhack.rs: -------------------------------------------------------------------------------- 1 | use html5ever::tree_builder::TreeSink; 2 | use html5ever::{driver::parse_document as parse_html, tendril::TendrilSink}; 3 | use markup5ever_rcdom::{Handle, RcDom, SerializableHandle}; 4 | use serde_json::json; 5 | use std::error::Error; 6 | use webkit6::{prelude::*, WebView}; 7 | 8 | fn decode_ms_string(b: &[u8]) -> Result> { 9 | let txt = match &b[0..2] { 10 | b"\xfe\xff" => { 11 | let bu16 = b 12 | .chunks_exact(2) 13 | .skip(1) // BOM 14 | .map(|c| u16::from_be_bytes(c.try_into().unwrap())) 15 | .collect::>(); 16 | String::from_utf16(&bu16)? 17 | } 18 | b"\xff\xfe" => { 19 | let bu16 = b 20 | .chunks_exact(2) 21 | .skip(1) // BOM 22 | .map(|c| u16::from_le_bytes(c.try_into().unwrap())) 23 | .collect::>(); 24 | String::from_utf16(&bu16)? 25 | } 26 | _ => String::from_utf8(b.to_vec())?, 27 | }; 28 | Ok(txt) 29 | } 30 | 31 | pub fn inject_polyfill(html: &[u8]) -> Result, Box> { 32 | let sys = sysinfo::System::new_all(); 33 | 34 | let html = decode_ms_string(html)? 35 | // Ideally, these namespaced elements are XML-parsed too. But the sources do not contain 36 | // namespace declaration which makes the parser abort with an error... 37 | .replace(" html > head).ok_or("missing ")?; 46 | // Drop the Content-Type meta tag. Gadget files are typically UTF16 encoded, which this tag 47 | // specifies. We re-encode it to UTF8. 48 | head.children 49 | .borrow_mut() 50 | .retain(|e| xml_query!(e, meta[http_equiv = "Content-Type"]).is_none()); 51 | 52 | // Insert the polyfill. 53 | head.children.borrow_mut().splice( 54 | 0..0, 55 | [ 56 | script_node(include_str!("polyfill.js")), 57 | script_node(format!("window.System.Machine = {};", machine_stats(&sys))), 58 | ], 59 | ); 60 | 61 | let mut buf = Vec::new(); 62 | html5ever::serialize::serialize( 63 | &mut buf, 64 | &SerializableHandle::from(dom.document), 65 | Default::default(), 66 | )?; 67 | Ok(buf) 68 | } 69 | 70 | fn script_node(src: impl AsRef) -> Handle { 71 | let parser = parse_html(RcDom::default(), Default::default()); 72 | let mut dom = parser.one(format!( 73 | r#""#, 74 | src.as_ref() 75 | )); 76 | let script = 77 | xml_query!(&dom.document, > html > head > script).expect("script not found in decoded DOM"); 78 | dom.remove_from_parent(&script); 79 | script 80 | } 81 | 82 | fn machine_stats(sys: &sysinfo::System) -> serde_json::Value { 83 | json!({ 84 | "CPUs": sys.cpus().iter() 85 | .map(|cpu| json!({"usagePercentage": cpu.cpu_usage()})) 86 | .collect::>(), 87 | "totalMemory": sys.total_memory() / 1_000_000, 88 | "availableMemory": sys.available_memory() / 1_000_000, 89 | }) 90 | } 91 | 92 | pub async fn update_machine_stats(web_view: &WebView, sys: &sysinfo::System) { 93 | let js = format!("window.System.Machine = {}", machine_stats(sys)); 94 | web_view 95 | .evaluate_javascript_future(&js, None, None) 96 | .await 97 | .unwrap(); 98 | } 99 | -------------------------------------------------------------------------------- /src/query.rs: -------------------------------------------------------------------------------- 1 | use markup5ever_rcdom::{Handle, NodeData}; 2 | 3 | pub fn get_name<'a>(n: &'a Handle) -> Option<&'a str> { 4 | match &n.data { 5 | NodeData::Element { name, .. } => Some(name.local.as_ref()), 6 | _ => None, 7 | } 8 | } 9 | 10 | pub fn get_attr<'a>(n: &'a Handle, key: &str) -> Option { 11 | let attrs = match &n.data { 12 | NodeData::Element { attrs, .. } => attrs.borrow(), 13 | _ => return None, 14 | }; 15 | attrs 16 | .iter() 17 | .filter(|a| a.name.local.as_ref() == key) 18 | .next() 19 | .map(|val| val.value.as_ref().to_string()) 20 | } 21 | 22 | pub fn get_text_contents(n: &Handle) -> Option { 23 | let strings = n 24 | .children 25 | .borrow() 26 | .iter() 27 | .filter_map(|c| match &c.data { 28 | NodeData::Text { contents } => Some(contents.borrow().as_ref().to_string()), 29 | _ => None, 30 | }) 31 | .collect::>(); 32 | if strings.is_empty() { 33 | None 34 | } else { 35 | Some(strings.join("")) 36 | } 37 | } 38 | 39 | macro_rules! xml_query { 40 | ($elem:expr, ) => { 41 | Some($elem) 42 | }; 43 | 44 | ($elem:expr, $name:ident $($t:tt)*) => { 45 | Some($elem) 46 | .filter(|e| crate::query::get_name(e) == Some(stringify!($name))) 47 | .and_then(|e| xml_query!(e, $($t)*)) 48 | }; 49 | 50 | ($elem:expr, > $($t:tt)+) => { 51 | $elem.children.borrow().iter().cloned() 52 | .filter_map(|e| xml_query!(e, $($t)+)) 53 | .next() 54 | }; 55 | 56 | ($elem:expr, [$k:ident=$v:expr] $($t:tt)*) => { 57 | Some($elem) 58 | .filter(|e| crate::query::get_attr(e, &stringify!($k).replace('_', "-")) 59 | .map(|v| v == $v) 60 | .unwrap_or(false)) 61 | .and_then(|e| xml_query!(e, $($t)*)) 62 | }; 63 | 64 | ($elem:expr, [$k:ident~=$v:expr] $($t:tt)*) => { 65 | Some($elem) 66 | .filter(|e| crate::query::get_attr(e, &stringify!($k).replace('_', "-")) 67 | .map(|v| v.to_lowercase() == $v.to_lowercase()) 68 | .unwrap_or(false)) 69 | .and_then(|e| xml_query!(e, $($t)*)) 70 | }; 71 | } 72 | 73 | #[cfg(test)] 74 | mod tests { 75 | use super::*; 76 | 77 | const TEST_HTML: &'static str = r#" 78 | 79 | 80 | 81 | Foo 82 | 83 | 84 | "#; 85 | 86 | #[test] 87 | fn test_macro_compiles_html5ever() { 88 | use html5ever::{driver::parse_document, tendril::TendrilSink}; 89 | use markup5ever_rcdom::RcDom; 90 | 91 | let parser = parse_document(RcDom::default(), Default::default()); 92 | let dom = parser.one(TEST_HTML).document; 93 | assert!(matches!(dom.data, NodeData::Document)); 94 | 95 | xml_query!(&dom, > html > head).unwrap(); 96 | xml_query!(&dom, > html > head > title).unwrap(); 97 | xml_query!(&dom, > html > head > meta[http_equiv="Content-Type"]).unwrap(); 98 | xml_query!(&dom, > html > head > meta[http_equiv~="content-type"]).unwrap(); 99 | } 100 | 101 | #[test] 102 | fn test_macro_compiles_xml5ever() { 103 | use markup5ever_rcdom::RcDom; 104 | use xml5ever::{driver::parse_document, tendril::TendrilSink}; 105 | 106 | let parser = parse_document(RcDom::default(), Default::default()); 107 | let dom = parser.one(TEST_HTML).document; 108 | 109 | xml_query!(&dom, > html > head).unwrap(); 110 | xml_query!(&dom, > html > head).unwrap(); 111 | xml_query!(&dom, > html > head > title).unwrap(); 112 | xml_query!(&dom, > html > head > meta[http_equiv="Content-Type"]).unwrap(); 113 | xml_query!(&dom, > html > head > meta[http_equiv~="content-type"]).unwrap(); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/gadget.rs: -------------------------------------------------------------------------------- 1 | use crate::query; 2 | use crate::webhack; 3 | use markup5ever_rcdom::RcDom; 4 | use std::error::Error; 5 | use std::fmt; 6 | use std::fs::{create_dir_all, File}; 7 | use std::io::{self, Read, Write}; 8 | use std::path::{Path, PathBuf}; 9 | use xml5ever::{driver::parse_document as parse_xml, tendril::TendrilSink}; 10 | use zip::result::ZipError; 11 | use zip::{read::ZipFile, ZipArchive}; 12 | 13 | #[derive(Debug)] 14 | pub struct Gadget { 15 | ar: ZipArchive, 16 | 17 | name: String, 18 | author: Option, 19 | copyright: Option, 20 | 21 | entrypoint: PathBuf, 22 | } 23 | 24 | impl Gadget { 25 | pub fn from_file(filename: impl AsRef) -> Result> { 26 | let mut ar = ZipArchive::new(File::open(filename)?)?; 27 | 28 | let manifest_str = { 29 | let mut manifest_file = try_file_by_name(&mut ar, ["gadget.xml", "en-US/gadget.xml"]) 30 | .ok_or("gadget manifest not found")?; 31 | let mut manifest_str = String::with_capacity(manifest_file.size() as usize); 32 | manifest_file.read_to_string(&mut manifest_str)?; 33 | manifest_str 34 | }; 35 | 36 | let parser = parse_xml(RcDom::default(), Default::default()); 37 | let manifest = parser.one(manifest_str).document; 38 | 39 | let name = xml_query!(&manifest, > gadget > name) 40 | .and_then(|n| query::get_text_contents(&n)) 41 | .ok_or_else(|| "no gadget.name node")?; 42 | let author = xml_query!(&manifest, > gadget > author) 43 | .and_then(|n| query::get_attr(&n, "name").map(String::from)); 44 | let copyright = 45 | xml_query!(&manifest, > gadget > copyright).and_then(|n| query::get_text_contents(&n)); 46 | let entrypoint = xml_query!(&manifest, > gadget > hosts > host > base[type~="HTML"]) 47 | .and_then(|n| query::get_attr(&n, "src").map(String::from)) 48 | .ok_or_else(|| "no gadget html entrypoint node")?; 49 | 50 | Ok(Self { 51 | ar, 52 | name, 53 | author, 54 | copyright, 55 | entrypoint: PathBuf::from(entrypoint), 56 | }) 57 | } 58 | 59 | pub fn unpack_to(&mut self, path: impl AsRef) -> io::Result<()> { 60 | let path = path.as_ref(); 61 | 62 | for file_index in 0.. { 63 | let mut f = match self.ar.by_index(file_index) { 64 | Ok(v) => v, 65 | Err(ZipError::FileNotFound) => break, 66 | Err(err) => Err(err)?, 67 | }; 68 | if f.is_dir() { 69 | continue; 70 | } 71 | 72 | let fname = f 73 | .enclosed_name() 74 | .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "invalid file name"))?; 75 | // TODO: check for other lang-pairs? 76 | let fname = fname.strip_prefix("en-US/").unwrap_or(&fname); 77 | 78 | let is_entrypoint = fname == &self.entrypoint; 79 | let oname = path.join(if is_entrypoint { 80 | Path::new("index.html") 81 | } else { 82 | fname 83 | }); 84 | 85 | create_dir_all(oname.parent().unwrap())?; 86 | let mut of = File::create(oname)?; 87 | 88 | if is_entrypoint { 89 | let mut html = Vec::with_capacity(f.size() as usize); 90 | f.read_to_end(&mut html)?; 91 | html = webhack::inject_polyfill(&html) 92 | .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; 93 | of.write_all(&html)?; 94 | } else { 95 | io::copy(&mut f, &mut of)?; 96 | } 97 | } 98 | 99 | Ok(()) 100 | } 101 | } 102 | 103 | impl fmt::Display for Gadget { 104 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 105 | write!(f, "{}", &self.name)?; 106 | if let Some(author) = &self.author { 107 | write!(f, ", {}", author)?; 108 | if let Some(copyright) = &self.copyright { 109 | write!(f, " {}", copyright)?; 110 | } 111 | } 112 | Ok(()) 113 | } 114 | } 115 | 116 | fn try_file_by_name( 117 | ar: &mut ZipArchive, 118 | paths: impl IntoIterator>, 119 | ) -> Option { 120 | let i = paths 121 | .into_iter() 122 | .filter_map(|p| ar.index_for_path(p)) 123 | .next()?; 124 | ar.by_index(i).ok() 125 | } 126 | 127 | #[cfg(test)] 128 | mod tests { 129 | use super::*; 130 | 131 | #[test] 132 | fn gadget_from_file() { 133 | let f = "./testdata/cpu.gadget"; 134 | 135 | let gadget = Gadget::from_file(f).unwrap(); 136 | assert_eq!(gadget.name, "CPU Meter"); 137 | assert_eq!(gadget.entrypoint, Path::new("cpu.html")); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::{arg, command, value_parser}; 2 | use gadget::Gadget; 3 | use gtk4::gio::ApplicationFlags; 4 | use gtk4::prelude::*; 5 | use gtk4_layer_shell::{Edge, Layer, LayerShell}; 6 | use std::borrow::Cow; 7 | use std::cell::RefCell; 8 | use std::error::Error; 9 | use std::path::{self, Path, PathBuf}; 10 | use std::rc::Rc; 11 | use std::time::Duration; 12 | use tempfile::tempdir; 13 | use webkit6::{prelude::*, Settings, WebContext, WebView}; 14 | 15 | #[macro_use] 16 | mod query; 17 | mod gadget; 18 | mod webhack; 19 | 20 | fn app_main( 21 | application: >k4::Application, 22 | working_dir: impl AsRef, 23 | gadget_files: &[impl AsRef], 24 | ) -> Result<(), Box> { 25 | let working_dir = working_dir.as_ref(); 26 | 27 | let cssp = gtk4::CssProvider::new(); 28 | cssp.load_from_string(r#"window.background { background: unset; }"#); 29 | let display = gtk4::gdk::Display::default().unwrap(); 30 | gtk4::style_context_add_provider_for_display( 31 | &display, 32 | &cssp, 33 | gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION, 34 | ); 35 | 36 | let web_settings = Settings::new(); 37 | web_settings.set_enable_write_console_messages_to_stdout(true); 38 | 39 | let gadget_widgets: Vec = gadget_files 40 | .iter() 41 | .map(|f| -> Result> { 42 | let f = f.as_ref(); 43 | let mut gadget = Gadget::from_file(f)?; 44 | eprintln!("loaded gadget: {}", &gadget); 45 | 46 | let f_name = f 47 | .file_stem() 48 | .map(|s| s.to_string_lossy()) 49 | .unwrap_or(Cow::Borrowed("unnamed")); 50 | let web_root = working_dir.join(&*f_name).to_string_lossy().to_string(); 51 | 52 | gadget.unpack_to(&*web_root)?; 53 | 54 | let web_context = WebContext::new(); 55 | web_context.add_path_to_sandbox(working_dir, true); 56 | let web_view = WebView::builder() 57 | .settings(&web_settings) 58 | .web_context(&web_context) 59 | .sensitive(false) 60 | .build(); 61 | web_view.set_background_color(>k4::gdk::RGBA::new(0.0, 0.0, 0.0, 0.0)); 62 | web_view.load_uri(&format!("file://{}/index.html", web_root)); 63 | 64 | Ok(web_view) 65 | }) 66 | .collect::>()?; 67 | 68 | for gadget_widget in &gadget_widgets { 69 | let window = gtk4::ApplicationWindow::builder() 70 | .application(application) 71 | .decorated(false) 72 | .resizable(false) 73 | .can_focus(false) 74 | .default_width(1) 75 | .default_height(1) 76 | .child(gadget_widget) 77 | .build(); 78 | let window = Rc::new(window); 79 | 80 | window.init_layer_shell(); 81 | window.set_layer(Layer::Bottom); 82 | window.set_anchor(Edge::Left, true); 83 | window.set_anchor(Edge::Top, true); 84 | 85 | make_window_movable(&window); 86 | 87 | window.present(); 88 | } 89 | 90 | let ctx = gtk4::glib::MainContext::default(); 91 | ctx.spawn_local(async move { 92 | let mut sys = sysinfo::System::new_all(); 93 | loop { 94 | async_std::task::sleep(Duration::from_millis(1000)).await; 95 | 96 | sys.refresh_memory(); 97 | sys.refresh_cpu_usage(); 98 | 99 | for web_view in &gadget_widgets { 100 | let (w, h) = web_body_size(&web_view).await; 101 | web_view.set_size_request(w, h); 102 | 103 | webhack::update_machine_stats(&web_view, &sys).await; 104 | } 105 | } 106 | }); 107 | 108 | Ok(()) 109 | } 110 | 111 | fn main() -> gtk4::glib::ExitCode { 112 | let cli = command!() 113 | .arg( 114 | arg!(-g --gadget "path to one ore more .gadget files") 115 | .required(true) 116 | .num_args(1..) 117 | .value_parser(value_parser!(PathBuf)), 118 | ) 119 | .arg(arg!(--debug "Debug mode")); 120 | 121 | let app_id = concat!("com.github.polyfloyd.", env!("CARGO_PKG_NAME")); 122 | let application = gtk4::Application::new( 123 | Some(app_id), 124 | ApplicationFlags::default() | ApplicationFlags::HANDLES_COMMAND_LINE, 125 | ); 126 | 127 | let tmp = tempdir().unwrap(); 128 | 129 | let working_dir = tmp.path().to_path_buf(); 130 | application.connect_command_line(move |app, args| { 131 | let matches = cli.clone().get_matches_from(args.arguments()); 132 | 133 | let gadget_files: Vec<&PathBuf> = matches.get_many::("gadget").unwrap().collect(); 134 | let debug = matches.get_flag("debug"); 135 | 136 | let wd = if debug { 137 | &path::absolute("debug").unwrap() 138 | } else { 139 | &working_dir 140 | }; 141 | 142 | if let Err(err) = app_main(app, &wd, &gadget_files) { 143 | eprintln!("{}", err); 144 | 1 145 | } else { 146 | 0 147 | } 148 | }); 149 | 150 | application.run() 151 | } 152 | 153 | async fn web_body_size(web_view: &WebView) -> (i32, i32) { 154 | let js = r#" 155 | return new Promise((resolve, reject) => { 156 | resolve({w: document.body.offsetWidth, h: document.body.offsetHeight }); 157 | }); 158 | "#; 159 | let rs = web_view 160 | .call_async_javascript_function_future(js, None, None, None) 161 | .await; 162 | 163 | let v = rs.unwrap(); 164 | assert!(v.is_object()); 165 | let w = v.object_get_property("w").unwrap().to_double(); 166 | let h = v.object_get_property("h").unwrap().to_double(); 167 | 168 | (w as i32, h as i32) 169 | } 170 | 171 | fn make_window_movable(window_rc: &Rc) { 172 | let delta_prev_rc = Rc::new(RefCell::new(None)); 173 | 174 | let click = gtk4::GestureClick::builder().button(0).build(); 175 | let delta_prev = Rc::clone(&delta_prev_rc); 176 | click.connect_pressed(move |_ev, _npress, x, y| { 177 | *delta_prev.borrow_mut() = Some((x, y)); 178 | }); 179 | let delta_prev = Rc::clone(&delta_prev_rc); 180 | click.connect_released(move |_ev, _npress, _x, _y| { 181 | *delta_prev.borrow_mut() = None; 182 | }); 183 | window_rc.add_controller(click); 184 | 185 | let mc = gtk4::EventControllerMotion::default(); 186 | let delta_prev = delta_prev_rc; 187 | let window = Rc::clone(&window_rc); 188 | mc.connect_motion(move |_event, x, y| { 189 | let mut prev = delta_prev.borrow_mut(); 190 | let (dx, dy) = match *prev { 191 | None => return, 192 | Some((lx, ly)) => (x - lx, y - ly), 193 | }; 194 | *prev = Some((x, y)); 195 | 196 | let new_x = (window.margin(Edge::Left) as f64 + dx) as i32; 197 | let new_y = (window.margin(Edge::Top) as f64 + dy) as i32; 198 | window.set_margin(Edge::Left, new_x.max(0)); 199 | window.set_margin(Edge::Top, new_y.max(0)); 200 | }); 201 | window_rc.add_controller(mc); 202 | } 203 | --------------------------------------------------------------------------------