use std::collections::HashMap;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use iced::advanced::image as core_image;
use iced::advanced::{
self, layout,
renderer::{self},
widget::Tree,
Clipboard, Layout, Shell, Widget,
};
use iced::keyboard;
use iced::mouse::{self, Interaction};
use iced::{Element, Point, Size, Task};
use iced::{Event, Length, Rectangle};
use url::Url;
use crate::webview::{common, ON_ACTION_REQUIRED};
use crate::{engines, ImageInfo, PageType, ViewId};
#[cfg(any(feature = "servo", feature = "cef", feature = "blitz"))]
use crate::webview::shader_widget::WebViewPrimitive;
#[cfg(any(feature = "servo", feature = "cef", feature = "blitz"))]
use iced::widget::shader;
#[allow(missing_docs)]
#[derive(Debug, Clone, PartialEq)]
pub enum Action {
CloseView(ViewId),
CreateView(PageType),
GoBackward(ViewId),
GoForward(ViewId),
GoToUrl(ViewId, Url),
Refresh(ViewId),
SendKeyboardEvent(ViewId, keyboard::Event),
SendMouseEvent(ViewId, mouse::Event, Point),
Update(ViewId),
UpdateAll,
Resize(Size<u32>),
CopySelection(ViewId),
#[doc(hidden)]
FetchComplete(
ViewId,
String,
Result<(String, HashMap<String, String>), String>,
),
#[doc(hidden)]
ImageFetchComplete(ViewId, String, Result<Vec<u8>, String>, bool, u64),
#[doc(hidden)]
SetScaleFactor(f32),
}
pub struct WebView<Engine, Message>
where
Engine: engines::Engine,
{
engine: Engine,
view_size: Size<u32>,
scale_factor: f32,
on_close_view: Option<Box<dyn Fn(ViewId) -> Message>>,
on_create_view: Option<Box<dyn Fn(ViewId) -> Message>>,
on_url_change: Option<Box<dyn Fn(ViewId, String) -> Message>>,
urls: HashMap<ViewId, String>,
on_title_change: Option<Box<dyn Fn(ViewId, String) -> Message>>,
titles: HashMap<ViewId, String>,
on_copy: Option<Box<dyn Fn(String) -> Message>>,
action_mapper: Option<Arc<dyn Fn(Action) -> Message + Send + Sync>>,
inflight_images: usize,
fetched_images: usize,
nav_epochs: HashMap<ViewId, u64>,
scale_observer: Arc<AtomicU32>,
}
impl<Engine: engines::Engine + Default, Message: Send + Clone + 'static> Default
for WebView<Engine, Message>
{
fn default() -> Self {
Self::with_engine(Engine::default())
}
}
impl<Engine: engines::Engine, Message: Send + Clone + 'static> WebView<Engine, Message> {
pub fn with_engine(engine: Engine) -> Self {
WebView {
engine,
view_size: Size::new(1920, 1080),
scale_factor: 1.0,
on_close_view: None,
on_create_view: None,
on_url_change: None,
urls: HashMap::new(),
on_title_change: None,
titles: HashMap::new(),
on_copy: None,
action_mapper: None,
inflight_images: 0,
fetched_images: 0,
nav_epochs: HashMap::new(),
scale_observer: Arc::new(AtomicU32::new(0)),
}
}
}
impl<Engine: engines::Engine + Default, Message: Send + Clone + 'static> WebView<Engine, Message> {
pub fn new() -> Self {
Self::default()
}
pub fn set_scale_factor(&mut self, scale: f32) {
if (self.scale_factor - scale).abs() <= f32::EPSILON {
return;
}
self.scale_factor = scale;
self.engine.set_scale_factor(scale);
}
fn query_scale_factor(&self) -> Task<Message> {
common::query_scale_factor(&self.action_mapper, Action::SetScaleFactor)
}
pub fn on_create_view(mut self, on_create_view: impl Fn(usize) -> Message + 'static) -> Self {
self.on_create_view = Some(Box::new(on_create_view));
self
}
pub fn on_close_view(mut self, on_close_view: impl Fn(usize) -> Message + 'static) -> Self {
self.on_close_view = Some(Box::new(on_close_view));
self
}
pub fn on_url_change(
mut self,
on_url_change: impl Fn(ViewId, String) -> Message + 'static,
) -> Self {
self.on_url_change = Some(Box::new(on_url_change));
self
}
pub fn on_title_change(
mut self,
on_title_change: impl Fn(ViewId, String) -> Message + 'static,
) -> Self {
self.on_title_change = Some(Box::new(on_title_change));
self
}
pub fn on_copy(mut self, on_copy: impl Fn(String) -> Message + 'static) -> Self {
self.on_copy = Some(Box::new(on_copy));
self
}
pub fn on_action(mut self, mapper: impl Fn(Action) -> Message + Send + Sync + 'static) -> Self {
self.action_mapper = Some(Arc::new(mapper));
self
}
pub fn with_initial_size(mut self, size: Size<u32>) -> Self {
self.view_size = size;
self
}
pub fn update(&mut self, action: Action) -> Task<Message> {
let mut tasks = Vec::new();
if matches!(
action,
Action::Update(_)
| Action::UpdateAll
| Action::GoToUrl(..)
| Action::GoBackward(_)
| Action::GoForward(_)
| Action::Refresh(_)
| Action::FetchComplete(..)
) {
if let Some(on_url_change) = &self.on_url_change {
for (id, url) in self.urls.iter_mut() {
let engine_url = self.engine.get_url(*id);
if *url != engine_url {
tasks.push(Task::done(on_url_change(*id, engine_url.clone())));
*url = engine_url;
}
}
}
if let Some(on_title_change) = &self.on_title_change {
for (id, title) in self.titles.iter_mut() {
let engine_title = self.engine.get_title(*id);
if *title != engine_title {
tasks.push(Task::done(on_title_change(*id, engine_title.clone())));
*title = engine_title;
}
}
}
}
match action {
Action::CloseView(id) => {
self.engine.remove_view(id);
self.urls.remove(&id);
self.titles.remove(&id);
if let Some(on_view_close) = &self.on_close_view {
tasks.push(Task::done((on_view_close)(id)))
}
}
Action::CreateView(page_type) => {
let id = if let PageType::Url(url) = page_type {
if !self.engine.handles_urls() {
let id = self.engine.new_view(self.view_size, None);
self.engine.goto(id, PageType::Url(url.clone()));
#[cfg(any(feature = "litehtml", feature = "blitz"))]
if let Some(mapper) = &self.action_mapper {
tasks.push(common::fetch_html_task(
id,
url,
mapper.clone(),
Action::FetchComplete,
));
} else {
log::error!("{ON_ACTION_REQUIRED}");
}
#[cfg(not(any(feature = "litehtml", feature = "blitz")))]
log::error!("{ON_ACTION_REQUIRED}");
id
} else {
self.engine
.new_view(self.view_size, Some(PageType::Url(url)))
}
} else {
self.engine.new_view(self.view_size, Some(page_type))
};
self.urls.insert(id, String::new());
self.titles.insert(id, String::new());
if let Some(on_view_create) = &self.on_create_view {
tasks.push(Task::done((on_view_create)(id)))
}
tasks.push(self.query_scale_factor());
}
Action::GoBackward(id) => {
self.engine.go_back(id);
self.engine.request_render(id);
}
Action::GoForward(id) => {
self.engine.go_forward(id);
self.engine.request_render(id);
}
Action::GoToUrl(id, url) => {
common::begin_navigation(
&mut self.nav_epochs,
&mut self.inflight_images,
&mut self.fetched_images,
id,
);
let url_str = url.to_string();
self.engine.goto(id, PageType::Url(url_str.clone()));
#[cfg(any(feature = "litehtml", feature = "blitz"))]
if !self.engine.handles_urls() {
if let Some(mapper) = &self.action_mapper {
tasks.push(common::fetch_html_task(
id,
url_str,
mapper.clone(),
Action::FetchComplete,
));
} else {
log::error!("{ON_ACTION_REQUIRED}");
}
}
#[cfg(not(any(feature = "litehtml", feature = "blitz")))]
if !self.engine.handles_urls() {
log::error!("{ON_ACTION_REQUIRED}");
}
self.engine.request_render(id);
}
Action::Refresh(id) => {
self.engine.refresh(id);
self.engine.request_render(id);
}
Action::SendKeyboardEvent(id, event) => {
self.engine.handle_keyboard_event(id, event);
self.engine.request_render(id);
}
Action::SendMouseEvent(id, event, point) => {
self.engine.handle_mouse_event(id, point, event);
if let Some(href) = self.engine.take_anchor_click(id) {
let current = self.engine.get_url(id);
match common::resolve_anchor_click(&href, ¤t) {
Some(common::AnchorTarget::Fragment(fragment)) => {
self.engine.scroll_to_fragment(id, &fragment);
}
Some(common::AnchorTarget::Navigate(resolved)) => {
tasks.push(self.update(Action::GoToUrl(id, resolved)));
}
None => {}
}
}
return Task::batch(tasks);
}
Action::Update(id) => {
self.engine.update();
let observed = self.scale_observer.load(Ordering::Relaxed);
if observed != 0 {
self.set_scale_factor(f32::from_bits(observed));
}
self.engine.request_render(id);
if self.inflight_images == 0 {
self.engine.flush_staged_images(id, self.view_size);
}
#[cfg(any(feature = "litehtml", feature = "blitz"))]
if let Some(mapper) = &self.action_mapper {
common::dispatch_image_fetches(
&mut self.engine,
&self.nav_epochs,
&mut self.fetched_images,
&mut self.inflight_images,
mapper,
Action::ImageFetchComplete,
&mut tasks,
);
}
return Task::batch(tasks);
}
Action::UpdateAll => {
self.engine.update();
let observed = self.scale_observer.load(Ordering::Relaxed);
if observed != 0 {
self.set_scale_factor(f32::from_bits(observed));
}
if self.inflight_images == 0 {
for id in self.engine.view_ids() {
self.engine.flush_staged_images(id, self.view_size);
}
}
self.engine.render();
#[cfg(any(feature = "litehtml", feature = "blitz"))]
if let Some(mapper) = &self.action_mapper {
common::dispatch_image_fetches(
&mut self.engine,
&self.nav_epochs,
&mut self.fetched_images,
&mut self.inflight_images,
mapper,
Action::ImageFetchComplete,
&mut tasks,
);
}
return Task::batch(tasks);
}
Action::Resize(size) => {
if self.view_size != size {
self.view_size = size;
self.engine.resize(size);
tasks.push(self.query_scale_factor());
}
return Task::batch(tasks);
}
Action::CopySelection(id) => {
if let Some(text) = self.engine.get_selected_text(id) {
if let Some(on_copy) = &self.on_copy {
tasks.push(Task::done((on_copy)(text)));
}
}
return Task::batch(tasks);
}
Action::FetchComplete(view_id, url, result) => {
if !common::handle_fetch_complete(&mut self.engine, view_id, &url, result) {
return Task::batch(tasks);
}
self.engine.request_render(view_id);
}
Action::ImageFetchComplete(view_id, src, result, redraw_on_ready, epoch) => {
common::handle_image_fetch_complete(
&mut self.engine,
&self.nav_epochs,
&mut self.inflight_images,
view_id,
&src,
&result,
redraw_on_ready,
epoch,
);
return Task::batch(tasks);
}
Action::SetScaleFactor(f) => {
self.set_scale_factor(f);
}
};
Task::batch(tasks)
}
pub fn url_for(&self, id: ViewId) -> Option<&str> {
self.urls.get(&id).map(|s| s.as_str())
}
pub fn title_for(&self, id: ViewId) -> Option<&str> {
self.titles.get(&id).map(|s| s.as_str())
}
pub fn view<'a, T: 'a>(&'a self, id: usize) -> Element<'a, Action, T> {
let content_height = self.engine.get_content_height(id);
if content_height > 0.0 {
WebViewWidget::new(
id,
self.view_size,
self.engine.get_view(id),
self.engine.get_cursor(id),
self.engine.get_selection_rects(id),
self.engine.get_scroll_y(id),
content_height,
)
.into()
} else {
#[cfg(any(feature = "servo", feature = "cef", feature = "blitz"))]
{
shader::Shader::new(AdvancedShaderProgram::new(
id,
self.engine.get_view(id),
self.engine.get_cursor(id),
self.scale_observer.clone(),
))
.width(Length::Fill)
.height(Length::Fill)
.into()
}
#[cfg(not(any(feature = "servo", feature = "cef", feature = "blitz")))]
{
WebViewWidget::new(
id,
self.view_size,
self.engine.get_view(id),
self.engine.get_cursor(id),
self.engine.get_selection_rects(id),
0.0,
0.0,
)
.into()
}
}
}
}
#[cfg(any(feature = "servo", feature = "cef", feature = "blitz"))]
struct AdvancedShaderProgram<'a> {
view_id: ViewId,
image_info: &'a ImageInfo,
cursor: Interaction,
scale_observer: Arc<AtomicU32>,
}
#[cfg(any(feature = "servo", feature = "cef", feature = "blitz"))]
impl<'a> AdvancedShaderProgram<'a> {
fn new(
view_id: ViewId,
image_info: &'a ImageInfo,
cursor: Interaction,
scale_observer: Arc<AtomicU32>,
) -> Self {
Self {
view_id,
image_info,
cursor,
scale_observer,
}
}
}
#[cfg(any(feature = "servo", feature = "cef", feature = "blitz"))]
#[derive(Default)]
struct AdvancedShaderState {
bounds: Size<u32>,
}
#[cfg(any(feature = "servo", feature = "cef", feature = "blitz"))]
impl<'a> shader::Program<Action> for AdvancedShaderProgram<'a> {
type State = AdvancedShaderState;
type Primitive = WebViewPrimitive;
fn update(
&self,
state: &mut Self::State,
event: &Event,
bounds: Rectangle,
cursor: mouse::Cursor,
) -> Option<shader::Action<Action>> {
let size = Size::new(bounds.width.round() as u32, bounds.height.round() as u32);
if state.bounds != size {
state.bounds = size;
return Some(shader::Action::publish(Action::Resize(size)));
}
match event {
Event::Keyboard(event) => {
if let keyboard::Event::KeyPressed {
key: keyboard::Key::Character(c),
modifiers,
..
} = event
{
if modifiers.command() && c.as_str() == "c" {
return Some(shader::Action::publish(Action::CopySelection(self.view_id)));
}
}
Some(shader::Action::publish(Action::SendKeyboardEvent(
self.view_id,
event.clone(),
)))
}
Event::Mouse(event) => {
if let Some(point) = cursor.position_in(bounds) {
Some(shader::Action::publish(Action::SendMouseEvent(
self.view_id,
*event,
point,
)))
} else if matches!(event, mouse::Event::CursorLeft) {
Some(shader::Action::publish(Action::SendMouseEvent(
self.view_id,
*event,
Point::ORIGIN,
)))
} else {
None
}
}
_ => None,
}
}
fn draw(
&self,
_state: &Self::State,
_cursor: mouse::Cursor,
_bounds: Rectangle,
) -> Self::Primitive {
WebViewPrimitive {
pixels: self.image_info.pixels(),
width: self.image_info.image_width(),
height: self.image_info.image_height(),
scale_observer: self.scale_observer.clone(),
}
}
fn mouse_interaction(
&self,
_state: &Self::State,
_bounds: Rectangle,
_cursor: mouse::Cursor,
) -> Interaction {
self.cursor
}
}
struct WebViewWidget<'a> {
id: ViewId,
bounds: Size<u32>,
handle: core_image::Handle,
cursor: Interaction,
selection_rects: &'a [[f32; 4]],
scroll_y: f32,
content_height: f32,
}
impl<'a> WebViewWidget<'a> {
#[allow(clippy::too_many_arguments)]
fn new(
id: ViewId,
bounds: Size<u32>,
image: &ImageInfo,
cursor: Interaction,
selection_rects: &'a [[f32; 4]],
scroll_y: f32,
content_height: f32,
) -> Self {
Self {
id,
bounds,
handle: image.as_handle(),
cursor,
selection_rects,
scroll_y,
content_height,
}
}
}
impl<'a, Renderer, Theme> Widget<Action, Theme, Renderer> for WebViewWidget<'a>
where
Renderer: iced::advanced::Renderer
+ iced::advanced::image::Renderer<Handle = iced::advanced::image::Handle>,
{
fn size(&self) -> Size<Length> {
Size {
width: Length::Fill,
height: Length::Fill,
}
}
fn layout(
&mut self,
_tree: &mut Tree,
_renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout::Node::new(limits.max())
}
fn draw(
&self,
_tree: &Tree,
renderer: &mut Renderer,
_theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor: mouse::Cursor,
viewport: &Rectangle,
) {
let bounds = layout.bounds();
if self.content_height > 0.0 {
renderer.with_layer(bounds, |renderer| {
let image_bounds = Rectangle {
x: bounds.x,
y: bounds.y - self.scroll_y,
width: bounds.width,
height: self.content_height,
};
renderer.draw_image(
core_image::Image::new(self.handle.clone())
.snap(true)
.filter_method(core_image::FilterMethod::Nearest),
image_bounds,
*viewport,
);
});
} else {
renderer.draw_image(
core_image::Image::new(self.handle.clone())
.snap(true)
.filter_method(core_image::FilterMethod::Nearest),
bounds,
*viewport,
);
}
if !self.selection_rects.is_empty() {
let rects = self.selection_rects;
let scroll_y = self.scroll_y;
renderer.with_layer(bounds, |renderer| {
let highlight = iced::Color::from_rgba(0.26, 0.52, 0.96, 0.3);
for rect in rects {
let quad_bounds = Rectangle {
x: bounds.x + rect[0],
y: bounds.y + rect[1] - scroll_y,
width: rect[2],
height: rect[3],
};
renderer.fill_quad(
renderer::Quad {
bounds: quad_bounds,
..renderer::Quad::default()
},
highlight,
);
}
});
}
}
fn update(
&mut self,
_state: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Action>,
_viewport: &Rectangle,
) {
let size = Size::new(
layout.bounds().width.round() as u32,
layout.bounds().height.round() as u32,
);
if self.bounds != size {
self.bounds = size;
shell.publish(Action::Resize(size));
}
match event {
Event::Keyboard(event) => {
if let keyboard::Event::KeyPressed {
key: keyboard::Key::Character(c),
modifiers,
..
} = event
{
if modifiers.command() && c.as_str() == "c" {
shell.publish(Action::CopySelection(self.id));
}
}
shell.publish(Action::SendKeyboardEvent(self.id, event.clone()));
}
Event::Mouse(event) => {
if let Some(point) = cursor.position_in(layout.bounds()) {
shell.publish(Action::SendMouseEvent(self.id, *event, point));
} else if matches!(event, mouse::Event::CursorLeft) {
shell.publish(Action::SendMouseEvent(self.id, *event, Point::ORIGIN));
}
}
_ => (),
}
}
fn mouse_interaction(
&self,
_state: &Tree,
layout: Layout<'_>,
cursor: mouse::Cursor,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
if cursor.is_over(layout.bounds()) {
self.cursor
} else {
mouse::Interaction::Idle
}
}
}
impl<'a, Message: 'a, Renderer, Theme> From<WebViewWidget<'a>>
for Element<'a, Message, Theme, Renderer>
where
Renderer: advanced::Renderer + advanced::image::Renderer<Handle = advanced::image::Handle>,
WebViewWidget<'a>: Widget<Message, Theme, Renderer>,
{
fn from(widget: WebViewWidget<'a>) -> Self {
Self::new(widget)
}
}