Skip to content

Commit 7f549aa

Browse files
committed
feat: event-driven servo wake subscription
1 parent f2b717b commit 7f549aa

8 files changed

Lines changed: 137 additions & 21 deletions

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog
22

3+
## [0.1.7] - 2026-04-21
4+
5+
### Added
6+
- Servo engine: event-driven wake subscription via `webview.subscription()` — replaces hardcoded `time::every(...)` polling
7+
- `ServoWaker` signals the embedder only when Servo has work, with a 500ms fallback tick as a safety net
8+
- Examples updated to use the new Servo subscription when the `servo` feature is enabled
9+
10+
### Changed
11+
- `servo` feature now depends on `tokio`; `tokio` dependency gains the `sync` feature for `Notify`
12+
313
## [0.1.6] - 2026-04-03
414

515
### Added

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "iced_webview_v2"
3-
version = "0.1.6"
3+
version = "0.1.7"
44
edition = "2021"
55
rust-version = "1.90.0"
66
description = "An easily embedded webview library for iced"
@@ -40,7 +40,7 @@ blitz = [
4040
"dep:tokio",
4141
]
4242
litehtml = ["dep:litehtml", "dep:reqwest"]
43-
servo = ["dep:servo", "dep:urlencoding", "dep:rustls", "dep:euclid", "dep:keyboard-types-servo", "dep:dpi"]
43+
servo = ["dep:servo", "dep:urlencoding", "dep:rustls", "dep:euclid", "dep:keyboard-types-servo", "dep:dpi", "dep:tokio"]
4444
cef = ["dep:cef", "dep:urlencoding"]
4545
docs_only = []
4646

@@ -63,7 +63,7 @@ peniko = { version = "0.6", optional = true }
6363
cursor-icon = { version = "1", optional = true }
6464
keyboard-types = { version = "0.7", optional = true }
6565
smol_str = { version = "0.3", optional = true }
66-
tokio = { version = "1", features = ["rt"], optional = true }
66+
tokio = { version = "1", features = ["rt", "sync"], optional = true }
6767

6868
# Servo engine deps
6969
servo = { version = "0.1", optional = true }

examples/email.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
use iced::{
2-
time,
32
widget::{column, text},
43
Element, Subscription, Task,
54
};
65
use iced_webview::{Action, PageType, WebView};
6+
7+
#[cfg(not(feature = "servo"))]
8+
use iced::time;
9+
#[cfg(not(feature = "servo"))]
710
use std::time::Duration;
811

912
#[cfg(feature = "cef")]
@@ -154,8 +157,15 @@ impl App {
154157
}
155158

156159
fn subscription(&self) -> Subscription<Message> {
157-
time::every(Duration::from_millis(16))
158-
.map(|_| Action::Update)
159-
.map(Message::WebView)
160+
#[cfg(feature = "servo")]
161+
{
162+
self.webview.subscription().map(Message::WebView)
163+
}
164+
#[cfg(not(feature = "servo"))]
165+
{
166+
time::every(Duration::from_millis(16))
167+
.map(|_| Action::Update)
168+
.map(Message::WebView)
169+
}
160170
}
161171
}

examples/embedded_webview.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
use iced::{
2-
time,
32
widget::{button, column, container, row, text},
43
Element, Length, Subscription, Task,
54
};
65
use iced_webview::{Action, PageType, WebView};
6+
7+
#[cfg(not(feature = "servo"))]
8+
use iced::time;
9+
#[cfg(not(feature = "servo"))]
710
use std::time::Duration;
811

912
#[cfg(feature = "cef")]
@@ -137,8 +140,15 @@ impl App {
137140
}
138141

139142
fn subscription(&self) -> Subscription<Message> {
140-
time::every(Duration::from_millis(10))
141-
.map(|_| Action::Update)
142-
.map(Message::WebView)
143+
#[cfg(feature = "servo")]
144+
{
145+
self.webview.subscription().map(Message::WebView)
146+
}
147+
#[cfg(not(feature = "servo"))]
148+
{
149+
time::every(Duration::from_millis(10))
150+
.map(|_| Action::Update)
151+
.map(Message::WebView)
152+
}
143153
}
144154
}

examples/webview.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
use iced::{time, Element, Subscription, Task};
1+
use iced::{Element, Subscription, Task};
22
use iced_webview::{Action, PageType, WebView};
3+
4+
#[cfg(not(feature = "servo"))]
5+
use iced::time;
6+
#[cfg(not(feature = "servo"))]
37
use std::time::Duration;
48

59
#[cfg(feature = "cef")]
@@ -76,8 +80,15 @@ impl App {
7680
}
7781

7882
fn subscription(&self) -> Subscription<Message> {
79-
time::every(Duration::from_millis(10))
80-
.map(|_| Action::Update)
81-
.map(Message::WebView)
83+
#[cfg(feature = "servo")]
84+
{
85+
self.webview.subscription().map(Message::WebView)
86+
}
87+
#[cfg(not(feature = "servo"))]
88+
{
89+
time::every(Duration::from_millis(10))
90+
.map(|_| Action::Update)
91+
.map(Message::WebView)
92+
}
8293
}
8394
}

src/engines/servo.rs

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
use std::cell::RefCell;
2+
use std::hash::{Hash, Hasher};
23
use std::rc::Rc;
4+
use std::sync::Arc;
5+
use std::time::Duration;
36

47
use iced::keyboard;
58
use iced::mouse::{self, Interaction};
69
use iced::{Point, Size};
10+
use tokio::sync::Notify;
711

812
use super::{Engine, PageType, PixelFormat, ViewId, ViewManager};
913
use crate::ImageInfo;
@@ -20,12 +24,35 @@ use servo::{
2024
};
2125
use url::Url;
2226

23-
/// No-op waker — the iced subscription tick already drives `spin_event_loop`.
24-
struct NoOpWaker;
27+
/// Event-driven waker that Servo calls (from any thread) whenever it wants the
28+
/// embedder to spin its event loop. The `Notify` coalesces multiple wake signals
29+
/// into a single pending notification, so a burst of calls produces at most one
30+
/// wake-up on the consumer side.
31+
#[derive(Clone)]
32+
struct ServoWaker {
33+
notify: Arc<Notify>,
34+
}
2535

26-
impl servo::EventLoopWaker for NoOpWaker {
36+
impl servo::EventLoopWaker for ServoWaker {
2737
fn clone_box(&self) -> Box<dyn servo::EventLoopWaker> {
28-
Box::new(NoOpWaker)
38+
Box::new(self.clone())
39+
}
40+
41+
fn wake(&self) {
42+
self.notify.notify_one();
43+
}
44+
}
45+
46+
/// Hashable handle used to give the Servo wake subscription a stable identity
47+
/// in the iced runtime. Hashing the `Arc`'s pointer address is enough —
48+
/// each `Servo` instance has its own `Notify`, so two different engines get
49+
/// two distinct subscriptions.
50+
#[derive(Clone)]
51+
struct WakeSubId(Arc<Notify>);
52+
53+
impl Hash for WakeSubId {
54+
fn hash<H: Hasher>(&self, state: &mut H) {
55+
(Arc::as_ptr(&self.0) as usize).hash(state);
2956
}
3057
}
3158

@@ -92,6 +119,10 @@ pub struct Servo {
92119
rendering_context: Rc<SoftwareRenderingContext>,
93120
views: ViewManager<ServoView>,
94121
scale_factor: f32,
122+
/// Shared with `ServoWaker` — Servo signals this whenever it wants the
123+
/// embedder to call `spin_event_loop`. The iced subscription exposed by
124+
/// [`Servo::subscription`] awaits the same handle.
125+
notify: Arc<Notify>,
95126
}
96127

97128
impl Default for Servo {
@@ -101,19 +132,52 @@ impl Default for Servo {
101132
SoftwareRenderingContext::new(size).expect("failed to create SoftwareRenderingContext");
102133
let rendering_context = Rc::new(rendering_context);
103134

135+
let notify = Arc::new(Notify::new());
136+
let waker = ServoWaker {
137+
notify: Arc::clone(&notify),
138+
};
139+
104140
let instance = ServoBuilder::default()
105-
.event_loop_waker(Box::new(NoOpWaker))
141+
.event_loop_waker(Box::new(waker))
106142
.build();
107143

108144
Self {
109145
instance,
110146
rendering_context,
111147
views: ViewManager::default(),
112148
scale_factor: 1.0,
149+
notify,
113150
}
114151
}
115152
}
116153

154+
impl Servo {
155+
/// An iced [`Subscription`] that yields [`Action::Update`] whenever Servo
156+
/// signals it has work to do, plus a 500ms safety tick so the event loop
157+
/// still runs if a wake is somehow missed. This replaces the hardcoded
158+
/// `time::every(10ms)` pattern used for other engines.
159+
///
160+
/// [`Subscription`]: iced::Subscription
161+
/// [`Action::Update`]: crate::Action::Update
162+
pub fn subscription(&self) -> iced::Subscription<crate::Action> {
163+
use iced::futures::SinkExt;
164+
165+
let id = WakeSubId(Arc::clone(&self.notify));
166+
167+
let wake_stream = iced::Subscription::run_with(id, |id| {
168+
let notify = Arc::clone(&id.0);
169+
iced::stream::channel(1, async move |mut output| loop {
170+
notify.notified().await;
171+
let _ = output.send(crate::Action::Update).await;
172+
})
173+
});
174+
175+
let fallback = iced::time::every(Duration::from_millis(500)).map(|_| crate::Action::Update);
176+
177+
iced::Subscription::batch([wake_stream, fallback])
178+
}
179+
}
180+
117181
fn cursor_to_interaction(cursor: Cursor) -> Interaction {
118182
match cursor {
119183
Cursor::Pointer => Interaction::Pointer,

src/webview/basic.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,17 @@ impl<Engine: engines::Engine + Default, Message: Send + Clone + 'static> WebView
588588
}
589589
}
590590

591+
#[cfg(feature = "servo")]
592+
impl<Message: Send + Clone + 'static> WebView<crate::engines::servo::Servo, Message> {
593+
/// Event-driven subscription for the Servo engine — yields
594+
/// [`Action::Update`] whenever Servo wakes the embedder, with a 500ms
595+
/// fallback tick. Use this in place of a hardcoded `time::every(...)`
596+
/// timer when running with the `servo` feature.
597+
pub fn subscription(&self) -> iced::Subscription<Action> {
598+
self.engine.subscription()
599+
}
600+
}
601+
591602
struct WebViewWidget<'a> {
592603
handle: core_image::Handle,
593604
cursor: Interaction,

0 commit comments

Comments
 (0)