freya_webview/
plugin.rs

1//! WebView plugin for managing WRY WebViews.
2
3use std::collections::HashMap;
4
5use freya_winit::{
6    plugins::{
7        FreyaPlugin,
8        PluginEvent,
9        PluginHandle,
10    },
11    winit::window::Window,
12};
13use torin::prelude::Area;
14use wry::{
15    Rect,
16    WebView as WryWebView,
17    WebViewBuilder,
18};
19
20use crate::{
21    lifecycle::{
22        WebViewEvents,
23        WebViewLifecycleEvent,
24    },
25    registry::{
26        WebViewConfig,
27        WebViewId,
28    },
29};
30
31/// State for a managed WebView.
32struct WebViewState {
33    webview: WryWebView,
34}
35
36/// Plugin for managing WebView instances.
37///
38/// This plugin handles the lifecycle of WRY WebViews, creating them when
39/// elements are added and destroying them when elements are removed.
40pub struct WebViewPlugin {
41    webviews: HashMap<WebViewId, WebViewState>,
42    events: WebViewEvents,
43}
44
45impl Default for WebViewPlugin {
46    fn default() -> Self {
47        Self::new()
48    }
49}
50
51impl WebViewPlugin {
52    /// Create a new WebView plugin.
53    pub fn new() -> Self {
54        Self {
55            webviews: HashMap::new(),
56            events: WebViewEvents::default(),
57        }
58    }
59
60    #[cfg(target_os = "linux")]
61    fn ensure_gtk_initialized(&mut self) {
62        // Initialize GTK for WebKitGTK on Linux
63        if !gtk::is_initialized() {
64            if gtk::init().is_ok() {
65                tracing::debug!("WebViewPlugin: GTK initialized");
66            } else {
67                tracing::error!("WebViewPlugin: Failed to initialize GTK");
68            }
69        }
70    }
71
72    #[cfg(target_os = "linux")]
73    fn advance_gtk_event_loop(&self) -> bool {
74        if gtk::is_initialized() {
75            // Process pending GTK events and return true if any were processed
76            let mut processed = false;
77            while gtk::events_pending() {
78                gtk::main_iteration_do(false);
79                processed = true;
80            }
81            processed
82        } else {
83            false
84        }
85    }
86
87    fn create_webview(
88        &mut self,
89        window: &Window,
90        id: WebViewId,
91        config: &WebViewConfig,
92        area: &Area,
93    ) {
94        tracing::debug!(
95            "WebViewPlugin: Creating WebView {:?} with URL {}",
96            id,
97            config.url
98        );
99
100        let mut builder = WebViewBuilder::new();
101
102        if !config.url.is_empty() {
103            builder = builder.with_url(&config.url);
104        }
105
106        if config.transparent {
107            builder = builder.with_transparent(true);
108        }
109
110        if let Some(ref user_agent) = config.user_agent {
111            builder = builder.with_user_agent(user_agent);
112        }
113
114        if let Some(ref on_created) = config.on_created {
115            builder = (on_created)(builder)
116        }
117
118        // Set initial bounds
119        let bounds = Rect {
120            position: wry::dpi::Position::Logical(area.origin.to_f64().to_tuple().into()),
121            size: wry::dpi::Size::Logical(area.size.to_f64().to_tuple().into()),
122        };
123        tracing::info!(
124            "WebViewPlugin: Creating WebView with bounds: pos=({}, {}), size=({}, {})",
125            area.min_x(),
126            area.min_y(),
127            area.width(),
128            area.height()
129        );
130        builder = builder.with_bounds(bounds).with_visible(true);
131
132        // Build the WebView as a child of the window
133        match builder.build_as_child(window) {
134            Ok(webview) => {
135                // Ensure visibility after creation
136                let _ = webview.set_visible(true);
137
138                self.webviews.insert(id, WebViewState { webview });
139                tracing::debug!("WebViewPlugin: WebView {:?} created successfully", id);
140            }
141            Err(e) => {
142                tracing::error!("WebViewPlugin: Failed to create WebView {:?}: {}", id, e);
143            }
144        }
145    }
146}
147
148impl FreyaPlugin for WebViewPlugin {
149    fn on_event(&mut self, event: &mut PluginEvent, _handle: PluginHandle) {
150        match event {
151            PluginEvent::RunnerCreated { runner, .. } => {
152                runner.provide_root_context(|| self.events.clone());
153            }
154            PluginEvent::WindowCreated { .. } => {
155                // Window created, initialize GTK on Linux
156                tracing::debug!("WebViewPlugin: Window created");
157                #[cfg(target_os = "linux")]
158                self.ensure_gtk_initialized();
159            }
160
161            PluginEvent::FinishedMeasuringLayout { window, .. } => {
162                // First, drain all lifecycle events from the channel into a vec
163                let events: Vec<WebViewLifecycleEvent> =
164                    self.events.lock().unwrap().drain(..).collect();
165
166                // Then process the events
167                for event in events {
168                    match event {
169                        WebViewLifecycleEvent::Resized { id, area, config } => {
170                            tracing::debug!("WebViewPlugin: Received Resized event for {:?}", id);
171                            // Check if this is a pending webview that needs creation
172                            if let Some(state) = self.webviews.get_mut(&id) {
173                                if let Err(e) = state.webview.set_visible(true) {
174                                    tracing::error!(
175                                        "WebViewPlugin: Failed to set visible {:?}: {}",
176                                        id,
177                                        e
178                                    );
179                                }
180                                let rect = Rect {
181                                    position: wry::dpi::Position::Logical(
182                                        area.origin.to_f64().to_tuple().into(),
183                                    ),
184                                    size: wry::dpi::Size::Logical(
185                                        area.size.to_f64().to_tuple().into(),
186                                    ),
187                                };
188
189                                let resize = match state.webview.bounds() {
190                                    Ok(r) => r != rect,
191                                    _ => true,
192                                };
193                                if resize && let Err(e) = state.webview.set_bounds(rect) {
194                                    tracing::error!(
195                                        "WebViewPlugin: Failed to set bounds for {:?}: {}",
196                                        id,
197                                        e
198                                    );
199                                }
200                            } else {
201                                self.create_webview(window, id, &config, &area);
202                            }
203                        }
204                        WebViewLifecycleEvent::Close { id } => {
205                            tracing::debug!("WebViewPlugin: Received Close event for {:?}", id);
206                            self.webviews.remove(&id);
207                        }
208                        WebViewLifecycleEvent::Hide { id } => {
209                            tracing::debug!("WebViewPlugin: Received Hide event for {:?}", id);
210                            if let Some(state) = self.webviews.get_mut(&id) {
211                                let _ = state.webview.set_visible(false);
212                            }
213                        }
214                    }
215                }
216            }
217
218            PluginEvent::WindowClosed { .. } => {
219                // Clean up all WebViews for this window
220                tracing::debug!(
221                    "WebViewPlugin: Window closed, cleaning up {} WebViews",
222                    self.webviews.len()
223                );
224                self.webviews.clear();
225            }
226
227            #[cfg(target_os = "linux")]
228            PluginEvent::AfterRedraw { window, .. } => {
229                // Advance GTK event loop on Linux to process WebView events
230                self.advance_gtk_event_loop();
231                // If we have WebViews, continuously request redraws to keep GTK events flowing
232                if !self.webviews.is_empty() {
233                    window.request_redraw();
234                }
235            }
236
237            PluginEvent::BeforeRender { .. } => {
238                // Also advance GTK event loop before rendering
239                #[cfg(target_os = "linux")]
240                {
241                    let _ = self.advance_gtk_event_loop();
242                }
243            }
244
245            _ => {}
246        }
247    }
248}