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!(