diff --git a/Cargo.lock b/Cargo.lock index 7ebab81..54489cd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1574,6 +1574,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + [[package]] name = "hermit-abi" version = "0.3.9" @@ -1736,9 +1742,9 @@ dependencies = [ [[package]] name = "iced" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44ccdb0d1a25ff7581e75991229857c353c87d79c199d375af37d264f6fbd06d" +checksum = "88acfabc84ec077eaf9ede3457ffa3a104626d79022a9bf7f296093b1d60c73f" dependencies = [ "iced_core", "iced_futures", @@ -1766,9 +1772,9 @@ dependencies = [ [[package]] name = "iced_core" -version = "0.13.0" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce264c157ad3968928d93f9b244ad8ad63465b5a5c31c86c13199b333629f16f" +checksum = "0013a238275494641bf8f1732a23a808196540dc67b22ff97099c044ae4c8a1c" dependencies = [ "bitflags 2.6.0", "bytes", @@ -1786,9 +1792,9 @@ dependencies = [ [[package]] name = "iced_futures" -version = "0.13.0" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b47bd5c48706c57004c8a2d4cb127cb4535600843edb13aed10b09c7cd55eda4" +checksum = "0c04a6745ba2e80f32cf01e034fd00d853aa4f4cd8b91888099cb7aaee0d5d7c" dependencies = [ "futures", "iced_core", @@ -1859,9 +1865,9 @@ dependencies = [ [[package]] name = "iced_runtime" -version = "0.13.0" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f72474ab379b1c53f4ec5e468c66f8e307f8db13c865c2714d2c4a4a5b38c9a1" +checksum = "348b5b2c61c934d88ca3b0ed1ed913291e923d086a66fa288ce9669da9ef62b5" dependencies = [ "bytes", "iced_core", @@ -1909,9 +1915,9 @@ dependencies = [ [[package]] name = "iced_widget" -version = "0.13.0" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e88dd57d414cc44427c523534b80e52a42b6828f0e27ad7b8478f839865ee3c" +checksum = "81429e1b950b0e4bca65be4c4278fea6678ea782030a411778f26fa9f8983e1d" dependencies = [ "iced_renderer", "iced_runtime", @@ -1956,6 +1962,8 @@ dependencies = [ "serde", "serde_json", "smol_str", + "strum", + "strum_macros", "tempfile", "ul-next", "url", @@ -2858,7 +2866,7 @@ version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39b0deead1528fd0e5947a8546a9642a9777c25f6e1e26f34c97b204bbb465bd" dependencies = [ - "heck", + "heck 0.4.1", "itertools 0.12.1", "proc-macro2", "proc-macro2-diagnostics", @@ -3153,9 +3161,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.36.1" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96a05e2e8efddfa51a84ca47cec303fac86c8541b686d37cac5efc0e094417bc" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" dependencies = [ "memchr", ] @@ -3438,6 +3446,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + [[package]] name = "rustybuzz" version = "0.14.1" @@ -3519,9 +3533,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.1" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" dependencies = [ "core-foundation-sys", "libc", @@ -3805,6 +3819,28 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.77", +] + [[package]] name = "subtle" version = "2.6.1" @@ -3913,18 +3949,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", @@ -4260,9 +4296,9 @@ checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524" [[package]] name = "unicode-script" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8d71f5726e5f285a935e9fe8edfd53f0491eb6e9a5774097fdabee7cd8c9cd" +checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f" [[package]] name = "unicode-segmentation" @@ -4272,15 +4308,15 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-xid" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "untrusted" @@ -4565,9 +4601,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.5" +version = "0.26.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bd24728e5af82c6c4ec1b66ac4844bdf8156257fccda846ec58b42cd0cdbe6a" +checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" dependencies = [ "rustls-pki-types", ] diff --git a/Cargo.toml b/Cargo.toml index dfa39c1..de977db 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "icy_browser" version = "0.1.0" edition = "2021" -rust-version = "1.79.0" +rust-version = "1.81.0" description = "iced browser widgets" repository = "https://github.com/LegitCamper/rust-browser" @@ -33,13 +33,19 @@ ultralight = ["ul-next"] [dependencies] env_home = "0.1.0" iced = { version = "0.13", features = ["advanced", "image", "tokio", "lazy"] } -iced_aw = { version = "0.10", features = ["tab_bar", "icons"] } +iced_aw = { version = "0.10", features = [ + "tab_bar", + "icons", + "selection_list", +] } iced_on_focus_widget = "0.1.1" rand = "0.8.5" reqwest = "0.12.5" serde = "1.0.207" serde_json = "1.0.124" smol_str = "0.2.2" +strum = { version = "0.26.3", features = ["derive"] } +strum_macros = "0.26.4" tempfile = "3.12.0" ul-next = { version = "0.4", optional = true } url = "2.5.2" diff --git a/README.md b/README.md index 3481eca..ab88561 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ### Supported Platforms -| Platform | Support | +| Platform | Support | | Windows | | | Linux | | diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..bd3bd7f --- /dev/null +++ b/build.rs @@ -0,0 +1,96 @@ +use std::fs::{self, DirEntry}; +use std::path::Path; + +const PATH: &str = env!("CARGO_MANIFEST_DIR"); + +fn main() { + // ensure runtime resources exist + #[cfg(feature = "ultralight")] + { + let mut possible_directories = Vec::new(); + + let target = Path::new(PATH).join("target"); + let debug_path = target.clone().join("debug"); + let release_path = target.clone().join("release"); + + if let Ok(debug) = fs::exists(debug_path.clone()) { + if debug { + get_paths( + &mut possible_directories, + debug_path.join("build").to_str().unwrap().to_string(), + ) + } + } else if let Ok(release) = fs::exists(release_path.clone()) { + if release { + get_paths( + &mut possible_directories, + release_path.join("build").to_str().unwrap().to_string(), + ) + } + } else { + panic!("Could not find either debug or release dirs") + } + + assert!(!possible_directories.is_empty()); + + let local_resources = Path::new(PATH).join("resources"); + + for path in possible_directories { + if let Ok(resources) = fs::exists(path.path().join("out/ul-sdk/resources")) { + if resources { + if let Ok(local_resources_exist) = fs::exists(local_resources.clone()) { + if local_resources_exist { + fs::remove_dir_all(local_resources.clone()) + .expect("Failed to delete resources dir") + } + } + + fs::create_dir(local_resources.clone()) + .expect("Failed to create resources dir"); + + copy_file( + path.path().join("out/ul-sdk/resources").as_path(), + local_resources.clone().join("").as_path(), + "cacert.pem", + ) + .expect("Failed to copy cacert.pem"); + copy_file( + path.path().join("out/ul-sdk/resources").as_path(), + local_resources.clone().join("").as_path(), + "icudt67l.dat", + ) + .expect("Failed to copy icudt67l.dat"); + + break; + } + } else { + panic!("The resouce dir entered has not resources") + } + } + } + + println!("cargo:rerun-if-changed=resources"); + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=Cargo.lock"); +} + +fn copy_file(from: &Path, to: &Path, file_name: &str) -> Result { + fs::copy(from.join(file_name), to.join(file_name)) +} + +fn get_paths(possible_paths: &mut Vec, path_str: String) { + let mut paths: Vec = fs::read_dir(path_str) + .expect("Could not read dir") + .map(|f| f.unwrap()) + .filter(|file| file.path().to_string_lossy().contains("ul-next-sys")) + .collect(); + // TODO: check if sort working + paths.sort_by(|a, b| { + a.metadata() + .unwrap() + .modified() + .unwrap() + .cmp(&b.metadata().unwrap().modified().unwrap()) + }); + possible_paths.append(&mut paths); +} diff --git a/examples/basic_browser.rs b/examples/basic_browser.rs index d27ad56..d1fa436 100644 --- a/examples/basic_browser.rs +++ b/examples/basic_browser.rs @@ -1,7 +1,9 @@ // Simple browser with familiar browser widget and the ultralight(webkit) webengine as a backend -use iced::{Element, Settings, Theme}; +use iced::Theme; +use iced::{Element, Settings, Subscription, Task}; use iced_aw::BOOTSTRAP_FONT_BYTES; +use std::time::Duration; use icy_browser::{widgets, BrowserWidget, Ultralight}; @@ -14,6 +16,7 @@ fn main() -> iced::Result { }; iced::application("Basic Browser Example", Browser::update, Browser::view) + .subscription(Browser::subscription) .settings(settings) .theme(|_| Theme::Dark) .run() @@ -21,35 +24,39 @@ fn main() -> iced::Result { #[derive(Debug, Clone)] pub enum Message { - BrowserWidget(widgets::Message), + BrowserWidget(widgets::Message), // Passes messagees to Browser widgets + Update, } struct Browser { widgets: BrowserWidget, } +impl Default for Browser { + fn default() -> Self { + // Customize the look and feel of the browser here + let widgets = BrowserWidget::new_with_ultralight() + .with_tab_bar() + .with_nav_bar() + .build(); + + Self { widgets } + } +} + impl Browser { - fn update(&mut self, message: Message) { + fn update(&mut self, message: Message) -> Task { match message { - Message::BrowserWidget(msg) => { - self.widgets.update(msg); - } + Message::BrowserWidget(msg) => self.widgets.update(msg).map(Message::BrowserWidget), + Message::Update => self.widgets.force_update().map(Message::BrowserWidget), } } fn view(&self) -> Element { self.widgets.view().map(Message::BrowserWidget) } -} - -impl Default for Browser { - fn default() -> Self { - let widgets = BrowserWidget::new_with_ultralight() - .with_tab_bar() - .with_nav_bar() - .with_browsesr_view() - .build(); - Self { widgets } + fn subscription(&self) -> Subscription { + iced::time::every(Duration::from_millis(10)).map(move |_| Message::Update) } } diff --git a/examples/keyboard_driven.rs b/examples/keyboard_driven.rs new file mode 100644 index 0000000..1fd4b63 --- /dev/null +++ b/examples/keyboard_driven.rs @@ -0,0 +1,84 @@ +// Simple keybaord driven browser using the ultralight(webkit) webengine as a backend + +use iced::event::{self, Event}; +use iced::Theme; +use iced::{Element, Settings, Subscription, Task}; +use iced_aw::BOOTSTRAP_FONT_BYTES; +use std::time::Duration; + +use icy_browser::{ + widgets, BrowserWidget, KeyType, Message as WidgetMessage, ShortcutBuilder, ShortcutModifier, + Ultralight, +}; + +fn main() -> iced::Result { + // This imports `icons` for widgets + let bootstrap_font = BOOTSTRAP_FONT_BYTES.into(); + let settings = Settings { + fonts: vec![bootstrap_font], + ..Default::default() + }; + + iced::application("Keyboard Driven Browser", Browser::update, Browser::view) + .subscription(Browser::subscription) + .settings(settings) + .theme(|_| Theme::Dark) + .run() +} + +#[derive(Debug, Clone)] +pub enum Message { + BrowserWidget(widgets::Message), // Passes messagees to Browser widgets + Update, + Event(Event), +} + +struct Browser { + widgets: BrowserWidget, +} + +impl Default for Browser { + fn default() -> Self { + let shortcuts = ShortcutBuilder::new() + .add_shortcut( + WidgetMessage::ToggleOverlay, + vec![ + KeyType::Modifier(ShortcutModifier::Ctrl), + KeyType::Key(iced::keyboard::Key::Character("e".into())), + ], + ) + .build(); + let widgets = BrowserWidget::new_with_ultralight() + .with_custom_shortcuts(shortcuts) + .with_tab_bar() + .with_nav_bar() + .build(); + + Self { widgets } + } +} + +impl Browser { + fn update(&mut self, message: Message) -> Task { + match message { + Message::BrowserWidget(msg) => self.widgets.update(msg).map(Message::BrowserWidget), + Message::Update => self.widgets.force_update().map(Message::BrowserWidget), + Message::Event(event) => self + .widgets + .update(widgets::Message::Event(Some(event))) + .map(Message::BrowserWidget), + } + } + + fn view(&self) -> Element { + self.widgets.view().map(Message::BrowserWidget) + } + + fn subscription(&self) -> Subscription { + Subscription::batch([ + iced::time::every(Duration::from_millis(10)).map(move |_| Message::Update), + // This is needed for child widgets such as overlay to detect Key events + event::listen().map(Message::Event), + ]) + } +} diff --git a/src/lib.rs b/src/lib.rs index 42f6fcc..6c47b35 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,10 @@ pub use engines::{BrowserEngine, PixelFormat, Tab, TabInfo, Tabs}; pub use engines::ultralight::Ultralight; pub mod widgets; -pub use widgets::{nav_bar, tab_bar, BrowserWidget}; +pub use widgets::{nav_bar, tab_bar, BrowserWidget, Message}; + +mod shortcut; +pub use shortcut::{KeyType, Shortcut, ShortcutBuilder, ShortcutModifier, Shortcuts}; // Image details for passing the view around #[derive(Debug, Clone)] @@ -33,7 +36,7 @@ impl ImageInfo { const WIDTH: u32 = 800; const HEIGHT: u32 = 800; - fn new(pixels: Vec, format: PixelFormat, width: u32, height: u32) -> Self { + pub fn new(pixels: Vec, format: PixelFormat, width: u32, height: u32) -> Self { // R, G, B, A assert_eq!(pixels.len() % 4, 0); diff --git a/src/shortcut.rs b/src/shortcut.rs new file mode 100644 index 0000000..83ee3ef --- /dev/null +++ b/src/shortcut.rs @@ -0,0 +1,108 @@ +use iced::keyboard::{Key, Modifiers}; + +use super::widgets::Message; + +pub struct ShortcutBuilder(Shortcuts); +impl ShortcutBuilder { + pub fn new() -> Self { + ShortcutBuilder(Vec::new()) + } + + pub fn add_shortcut(mut self, shortcut_action: Message, shortcut_keys: Vec) -> Self { + if self.0.iter().filter(|sc| sc.0 == shortcut_action).count() != 0 { + panic!("Tried to add a duplicated shortcut"); + } + + // Must have 1 char key + if shortcut_keys + .iter() + .map(|item| { + if let KeyType::Key(_) = item { + return true; + } else if let KeyType::Modifier(_) = item { + return false; + } + unreachable!() + }) + .filter(|item| *item) // if item == true + .count() + != 1 + { + panic!("Shortcuts MUST have ONLY one Charecter key") + } + + // Must have at least one modifier key + if shortcut_keys + .iter() + .map(|item| { + if let KeyType::Key(_) = item { + return false; + } else if let KeyType::Modifier(_) = item { + return true; + } + unreachable!() + }) + .filter(|item| *item) // if itme == true + .count() + < 1 + { + panic!("Shortcuts MUST have at least 1 Modifier key") + } + + self.0.push((shortcut_action, shortcut_keys)); + self + } + + pub fn build(self) -> Shortcuts { + self.0 + } +} + +impl Default for ShortcutBuilder { + fn default() -> Self { + Self::new() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum ShortcutModifier { + Shift, + Ctrl, + Alt, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum KeyType { + Key(iced::keyboard::Key), + Modifier(ShortcutModifier), +} +/// Configures Widget Keyboard Shortcut +pub type Shortcut = (Message, Vec); + +/// Configures Widget Keyboard Shortcuts +pub type Shortcuts = Vec; + +pub fn check_shortcut(shortcut: &Shortcut, key: &Key, modifiers: &Modifiers) -> bool { + shortcut + .1 + .iter() + .map(|s| match s { + KeyType::Key(s_key) => { + if let iced::keyboard::Key::Character(s_char) = s_key { + if let iced::keyboard::Key::Character(key_char) = key { + key_char == s_char + } else { + false + } + } else { + false + } + } + KeyType::Modifier(s_mod) => match s_mod { + ShortcutModifier::Shift => modifiers.shift(), + ShortcutModifier::Ctrl => modifiers.control(), + ShortcutModifier::Alt => modifiers.alt(), + }, + }) + .all(|s| s) // if s == true +} diff --git a/src/widgets/browser_view.rs b/src/widgets/browser_view.rs index 9271171..ac11234 100644 --- a/src/widgets/browser_view.rs +++ b/src/widgets/browser_view.rs @@ -8,47 +8,32 @@ use iced::advanced::{ }; use iced::event::Status; use iced::widget::image::{Handle, Image}; -use iced::{theme::Theme, Element, Event, Length, Point, Rectangle, Size}; +use iced::{theme::Theme, Element, Event, Length, Rectangle, Size}; +use super::Message; use crate::ImageInfo; -pub fn browser_view( - bounds: Size, - image: &ImageInfo, - send_bounds: Box) -> Message>, - keyboard_event: Box Message>, - mouse_event: Box Message>, -) -> BrowserView { - BrowserView::new(bounds, image, send_bounds, keyboard_event, mouse_event) +pub fn browser_view(bounds: Size, image: &ImageInfo, can_type: bool) -> BrowserView { + BrowserView::new(bounds, image, can_type) } -pub struct BrowserView { +pub struct BrowserView { bounds: Size, image: Image, - send_bounds: Box) -> Message>, - keyboard_event: Box Message>, - mouse_event: Box Message>, + can_interact: bool, // wheather or not to allow typing - useful when overlay enabled } -impl BrowserView { - pub fn new( - bounds: Size, - image: &ImageInfo, - send_bounds: Box) -> Message>, - keyboard_event: Box Message>, - mouse_event: Box Message>, - ) -> Self { +impl BrowserView { + pub fn new(bounds: Size, image: &ImageInfo, can_type: bool) -> Self { Self { bounds, image: image.as_image(), - send_bounds, - keyboard_event, - mouse_event, + can_interact: can_type, } } } -impl Widget for BrowserView +impl Widget for BrowserView where Renderer: iced::advanced::image::Renderer, { @@ -101,36 +86,36 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) -> event::Status { - // Send updates back if bounds change - // convert to u32 because Image takes u32 - let size = Size::new(layout.bounds().width as u32, layout.bounds().height as u32); - if self.bounds != size { - shell.publish((self.send_bounds)(size)); - } - - match event { - Event::Keyboard(event) => { - shell.publish((self.keyboard_event)(event)); - Status::Captured + if self.can_interact { + // Send updates back if bounds change + // convert to u32 because Image takes u32 + let size = Size::new(layout.bounds().width as u32, layout.bounds().height as u32); + if self.bounds != size { + shell.publish(Message::UpdateViewSize(size)); } - Event::Mouse(event) => { - if let Some(point) = cursor.position_in(layout.bounds()) { - shell.publish((self.mouse_event)(point, event)); - Status::Captured - } else { - Status::Ignored + + match event { + Event::Keyboard(event) => { + shell.publish(Message::SendKeyboardEvent(Some(event))); + } + Event::Mouse(event) => { + if let Some(point) = cursor.position_in(layout.bounds()) { + shell.publish(Message::SendMouseEvent(point, Some(event))); + } } + _ => (), } - _ => Status::Ignored, } + Status::Ignored } } -impl<'a, Message: 'a, Renderer> From> for Element<'a, Message, Theme, Renderer> +impl<'a, Message: 'a, Renderer> From for Element<'a, Message, Theme, Renderer> where Renderer: advanced::Renderer + advanced::image::Renderer, + BrowserView: Widget, { - fn from(widget: BrowserView) -> Self { + fn from(widget: BrowserView) -> Self { Self::new(widget) } } diff --git a/src/widgets/command_window.rs b/src/widgets/command_window.rs new file mode 100644 index 0000000..99668be --- /dev/null +++ b/src/widgets/command_window.rs @@ -0,0 +1,74 @@ +use iced::widget::{center, column, container, mouse_area, opaque, stack, text_input}; +use iced::{border, Color, Element, Length, Theme}; +use iced_aw::SelectionList; +use strum::IntoEnumIterator; + +use super::Message; + +pub struct CommandWindowState { + pub query: String, + actions: Vec, + pub selected_action: String, + pub selected_index: usize, +} + +impl CommandWindowState { + pub fn new() -> Self { + Self { + query: String::new(), + actions: Message::iter().map(|e| e.clone().to_string()).collect(), + selected_action: String::new(), + selected_index: 0, + } + } +} + +impl Default for CommandWindowState { + fn default() -> Self { + Self::new() + } +} + +pub fn command_window<'a>( + base: impl Into>, + state: &'a CommandWindowState, +) -> Element<'a, Message> { + let window = container(column![ + text_input("Command Menu", &state.query).on_input(Message::QueryChanged), + SelectionList::new(&state.actions, Message::CommandSelectionChanged) + .width(Length::Fill) + .height(Length::Fill) + .style(|theme: &Theme, _| iced_aw::style::selection_list::Style { + text_color: theme.palette().text, + background: theme.palette().background.into(), + ..Default::default() + }), + ]) + .padding(10) + .center(600) + .style(|theme: &Theme| container::Style { + background: Some(theme.palette().background.into()), + border: border::rounded(10), + ..container::Style::default() + }); + + stack![ + base.into(), + opaque( + mouse_area(center(opaque(window)).style(|_theme| { + container::Style { + background: Some( + Color { + a: 0.8, + ..Color::BLACK + } + .into(), + ), + ..container::Style::default() + } + })) + .on_press(Message::HideOverlay) + ) + ] + .into() +} diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 9df6c62..993fd37 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -1,6 +1,11 @@ +use command_window::CommandWindowState; use iced::keyboard::{self, key}; -use iced::{event::Event, mouse, widget::column, Element, Point, Size}; +use iced::widget::{self, column}; +use iced::{event::Event, mouse, Element, Point, Size, Task}; use iced_on_focus_widget::hoverable; +use nav_bar::NavBarState; +use std::string::ToString; +use strum_macros::{Display, EnumIter}; use url::Url; mod browser_view; @@ -12,42 +17,71 @@ pub use nav_bar::nav_bar; mod tab_bar; pub use tab_bar::tab_bar; -use crate::{engines::BrowserEngine, to_url, ImageInfo}; +mod command_window; +pub use command_window::command_window; -#[derive(Debug, Clone)] +use crate::{engines::BrowserEngine, shortcut::check_shortcut, to_url, ImageInfo, Shortcuts}; + +// Options exist only to have defaults for EnumIter +#[derive(Debug, Clone, PartialEq, Display, EnumIter)] pub enum Message { + // Commands + #[strum(to_string = "Go Backward")] GoBackward, + #[strum(to_string = "Go Forward")] GoForward, Refresh, + #[strum(to_string = "Go Home")] GoHome, + #[strum(to_string = "Go To Url")] GoToUrl(String), + #[strum(to_string = "Change Tab")] ChangeTab(TabSelectionType), + #[strum(to_string = "Close Tab")] CloseTab(TabSelectionType), + #[strum(to_string = "Close Tab")] + CloseCurrentTab, + #[strum(to_string = "New Tab")] CreateTab, + #[strum(to_string = "Toggle Command Palatte")] + ToggleOverlay, + #[strum(to_string = "Show Command Palatte")] + ShowOverlay, + #[strum(to_string = "Hide Command Palatte")] + HideOverlay, + + // Internal only - for widgets UrlChanged(String), UpdateUrl, - SendKeyboardEvent(keyboard::Event), - SendMouseEvent(Point, mouse::Event), + QueryChanged(String), + CommandSelectionChanged(usize, String), + SendKeyboardEvent(Option), + SendMouseEvent(Point, Option), UpdateViewSize(Size), - Event(Event), - ShowOverlay, - HideOverlay, + Event(Option), } /// Allows different widgets to interact in their native way -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum TabSelectionType { Id(u32), Index(usize), } +impl Default for TabSelectionType { + fn default() -> Self { + TabSelectionType::Index(0) + } +} pub struct BrowserWidget { engine: Option, home: Url, - url: String, - tab_bar: bool, - nav_bar: bool, - browser_view: bool, + nav_bar_state: NavBarState, + command_window_state: CommandWindowState, + with_tab_bar: bool, + with_nav_bar: bool, + show_overlay: bool, + shortcuts: Shortcuts, view_size: Size, } @@ -60,10 +94,12 @@ where Self { engine: None, home, - url: String::new(), - tab_bar: false, - nav_bar: false, - browser_view: false, + nav_bar_state: NavBarState::new(), + command_window_state: CommandWindowState::new(), + with_tab_bar: false, + with_nav_bar: false, + show_overlay: false, + shortcuts: Shortcuts::default(), view_size: Size::new(800, 800), } } @@ -101,17 +137,17 @@ where } pub fn with_tab_bar(mut self) -> Self { - self.tab_bar = true; + self.with_tab_bar = true; self } pub fn with_nav_bar(mut self) -> Self { - self.nav_bar = true; + self.with_nav_bar = true; self } - pub fn with_browsesr_view(mut self) -> Self { - self.browser_view = true; + pub fn with_custom_shortcuts(mut self, shortcuts: Shortcuts) -> Self { + self.shortcuts = shortcuts; self } @@ -119,7 +155,7 @@ where assert!(self.engine.is_some()); let mut build = Self { ..self }; - build.update(Message::CreateTab); + let _ = build.update(Message::CreateTab); // disregaurd task::none() for update build } @@ -135,19 +171,69 @@ where .expect("Browser was created without a backend engine!") } - pub fn update(&mut self, message: Message) { + fn update_engine(&mut self) { self.engine().do_work(); + if self.engine().has_loaded() { + if self.engine().need_render() { + let (format, image_data) = self.engine_mut().pixel_buffer(); + let view = ImageInfo::new( + image_data, + format, + self.view_size.width, + self.view_size.height, + ); + self.engine_mut() + .get_tabs_mut() + .get_current_mut() + .set_view(view) + } + } else { + let view = ImageInfo { + width: self.view_size.width, + height: self.view_size.height, + ..Default::default() + }; + self.engine_mut() + .get_tabs_mut() + .get_current_mut() + .set_view(view) + } + } + + /// This is used to periodically update browserview + pub fn force_update(&mut self) -> Task { + self.engine().do_work(); + let (format, image_data) = self.engine_mut().pixel_buffer(); + let view = ImageInfo::new( + image_data, + format, + self.view_size.width, + self.view_size.height, + ); + self.engine_mut() + .get_tabs_mut() + .get_current_mut() + .set_view(view); + + Task::none() + } - match message { + pub fn update(&mut self, message: Message) -> Task { + let task = match message { Message::UpdateViewSize(size) => { self.view_size = size; self.engine_mut().resize(size); + Task::none() } Message::SendKeyboardEvent(event) => { - self.engine().handle_keyboard_event(event); + self.engine() + .handle_keyboard_event(event.expect("Value cannot be none")); + Task::none() } Message::SendMouseEvent(point, event) => { - self.engine_mut().handle_mouse_event(point, event); + self.engine_mut() + .handle_mouse_event(point, event.expect("Value cannot be none")); + Task::none() } Message::ChangeTab(index_type) => { let id = match index_type { @@ -157,12 +243,16 @@ where } }; self.engine_mut().get_tabs_mut().set_current_id(id); - self.url = self.engine().get_tabs().get_current().url(); + self.nav_bar_state.0 = self.engine().get_tabs().get_current().url(); + Task::none() } + Message::CloseCurrentTab => Task::done(Message::CloseTab(TabSelectionType::Id( + self.engine().get_tabs().get_current_id(), + ))), Message::CloseTab(index_type) => { - // ensure there is still a tab + // ensure there is always at least one tab if self.engine().get_tabs().tabs().len() == 1 { - self.update(Message::CreateTab) + let _ = self.update(Message::CreateTab); // ignore task } let id = match index_type { @@ -172,10 +262,11 @@ where } }; self.engine_mut().get_tabs_mut().remove(id); - self.url = self.engine().get_tabs().get_current().url(); + self.nav_bar_state.0 = self.engine().get_tabs().get_current().url(); + Task::none() } Message::CreateTab => { - self.url = self.home.to_string(); + self.nav_bar_state.0 = self.home.to_string(); let home = self.home.clone(); let bounds = self.view_size; let tab = self.engine_mut().new_tab( @@ -187,89 +278,120 @@ where self.engine_mut().force_need_render(); self.engine_mut().resize(bounds); self.engine().goto_url(&home); + Task::none() } Message::GoBackward => { self.engine().go_back(); - self.url = self.engine().get_tabs().get_current().url(); + self.nav_bar_state.0 = self.engine().get_tabs().get_current().url(); + Task::none() } Message::GoForward => { self.engine().go_forward(); - self.url = self.engine().get_tabs().get_current().url(); + self.nav_bar_state.0 = self.engine().get_tabs().get_current().url(); + Task::none() + } + Message::Refresh => { + self.engine().refresh(); + Task::none() } - Message::Refresh => self.engine().refresh(), Message::GoHome => { self.engine().goto_url(&self.home); + Task::none() } Message::GoToUrl(url) => { self.engine().goto_url(&to_url(&url).unwrap()); + Task::none() } Message::UpdateUrl => { - self.url = self.engine().get_tabs().get_current().url(); + self.nav_bar_state.0 = self.engine().get_tabs().get_current().url(); + Task::none() + } + Message::UrlChanged(url) => { + self.nav_bar_state.0 = url; + Task::none() + } + Message::QueryChanged(query) => { + self.command_window_state.query = query; + Task::none() + } + Message::CommandSelectionChanged(index, name) => { + self.command_window_state.selected_index = index; + self.command_window_state.selected_action = name; + Task::none() + } + Message::ToggleOverlay => { + if self.show_overlay { + Task::done(Message::HideOverlay) + } else { + Task::done(Message::ShowOverlay) + } } - Message::UrlChanged(url) => self.url = url, Message::ShowOverlay => { - // self.show_modal = true; - // widget::focus_next() + self.show_overlay = true; + widget::focus_next() } Message::HideOverlay => { - // self.hide_modal(); + self.show_overlay = false; + widget::focus_next() } + Message::Event(event) => { + match event { + Some(Event::Keyboard(key)) => { + if let iced::keyboard::Event::KeyPressed { + key, + modified_key: _, + physical_key: _, + location: _, + modifiers, + text: _, + } = key + { + // Default behaviors + if key == keyboard::Key::Named(key::Named::Escape) && self.show_overlay + { + return Task::done(Message::HideOverlay); + } - Message::Event(event) => match event { - Event::Keyboard(keyboard::Event::KeyPressed { - key: keyboard::Key::Named(key::Named::Escape), - .. - }) => { - // self.hide_modal(); + // Shortcut (Customizable) behaviors + for shortcut in self.shortcuts.iter() { + if check_shortcut(shortcut, &key, &modifiers) { + return Task::done(shortcut.0.clone()); + } + } + } + Task::none() + } + // Other unwatched events + _ => Task::none(), } - _ => (), - }, - } - - if self.engine().has_loaded() { - if self.engine().need_render() { - let (format, image_data) = self.engine_mut().pixel_buffer(); - let view = ImageInfo::new( - image_data, - format, - self.view_size.width, - self.view_size.height, - ); - self.engine_mut() - .get_tabs_mut() - .get_current_mut() - .set_view(view) } - } else { - let view = ImageInfo { - width: self.view_size.width, - height: self.view_size.height, - ..Default::default() - }; - self.engine_mut() - .get_tabs_mut() - .get_current_mut() - .set_view(view) - } + }; + + self.update_engine(); + + task } pub fn view(&self) -> Element { let mut column = column![]; - if self.tab_bar { + if self.with_tab_bar { column = column.push(tab_bar(self.engine().get_tabs())) } - if self.nav_bar { - column = column.push(hoverable(nav_bar(&self.url)).on_unfocus(Message::UpdateUrl)) + if self.with_nav_bar { + column = column + .push(hoverable(nav_bar(&self.nav_bar_state)).on_focus_change(Message::UpdateUrl)) } - if self.browser_view { - column = column.push(browser_view( - self.view_size, - self.engine().get_tabs().get_current().get_view(), - Box::new(Message::UpdateViewSize), - Box::new(Message::SendKeyboardEvent), - Box::new(Message::SendMouseEvent), - )) + + let browser_view = browser_view( + self.view_size, + self.engine().get_tabs().get_current().get_view(), + !self.show_overlay, + ); + if self.show_overlay { + column = column.push(command_window(browser_view, &self.command_window_state)) + } else { + column = column.push(browser_view); } column.into() diff --git a/src/widgets/nav_bar.rs b/src/widgets/nav_bar.rs index 17974fc..e082e0d 100644 --- a/src/widgets/nav_bar.rs +++ b/src/widgets/nav_bar.rs @@ -4,7 +4,22 @@ use iced_aw::core::icons::bootstrap::{icon_to_text, Bootstrap}; use super::Message; -pub fn nav_bar(url: &str) -> Element { +/// Holds the state of infomation in nav_bar +pub struct NavBarState(pub String); + +impl NavBarState { + pub fn new() -> Self { + NavBarState(String::new()) + } +} + +impl Default for NavBarState { + fn default() -> Self { + Self::new() + } +} + +pub fn nav_bar(state: &NavBarState) -> Element { let back = tooltip_helper( Button::new(icon_to_text(Bootstrap::ChevronBarLeft)) .on_press(Message::GoBackward) @@ -31,10 +46,10 @@ pub fn nav_bar(url: &str) -> Element { ); let space_left = Space::new(Length::Fill, Length::Shrink); let space_right = Space::new(Length::Fill, Length::Shrink); - let search = text_input("https://site.com", url) + let search = text_input("https://site.com", &state.0) .on_input(Message::UrlChanged) .on_paste(Message::GoToUrl) - .on_submit(Message::GoToUrl(url.to_string())) + .on_submit(Message::GoToUrl(state.0.to_string())) .line_height(LineHeight::Relative(2.0)); row!(