mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
Many types used directly in the `libservo` API are in the `script_traits` crate, which was created to break circular dependencies. Move all API exposed types to `embedder_traits` which now contains types exposed via the `libservo` embedding API. Also expose these at the root of the `libservo` `servo` crate so that the API won't break when they move around in the future. The idea with `embedder_traits` in the future is that it contains types that are available throughout servo because they are used in the embedding API and thus should have minimal dependencies on other Servo crates (a bit like `base`). Signed-off-by: Martin Robinson <mrobinson@igalia.com>
594 lines
25 KiB
Rust
594 lines
25 KiB
Rust
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||
|
||
use std::cell::{Cell, RefCell};
|
||
use std::num::NonZeroU32;
|
||
use std::sync::Arc;
|
||
use std::time::Instant;
|
||
|
||
use egui::text::{CCursor, CCursorRange};
|
||
use egui::text_edit::TextEditState;
|
||
use egui::{
|
||
pos2, CentralPanel, Frame, Key, Label, Modifiers, PaintCallback, Pos2, SelectableLabel,
|
||
TopBottomPanel, Vec2,
|
||
};
|
||
use egui_glow::CallbackFn;
|
||
use egui_winit::EventResponse;
|
||
use euclid::{Box2D, Length, Point2D, Scale, Size2D};
|
||
use gleam::gl;
|
||
use glow::NativeFramebuffer;
|
||
use log::{trace, warn};
|
||
use servo::base::id::WebViewId;
|
||
use servo::compositing::windowing::EmbedderEvent;
|
||
use servo::servo_geometry::DeviceIndependentPixel;
|
||
use servo::servo_url::ServoUrl;
|
||
use servo::webrender_api::units::DevicePixel;
|
||
use servo::webrender_traits::SurfmanRenderingContext;
|
||
use servo::{TopLevelBrowsingContextId, TraversalDirection};
|
||
use winit::event::{ElementState, MouseButton, WindowEvent};
|
||
use winit::event_loop::ActiveEventLoop;
|
||
use winit::window::Window;
|
||
|
||
use super::egui_glue::EguiGlow;
|
||
use super::geometry::winit_position_to_euclid_point;
|
||
use super::webview::{LoadStatus, WebViewManager};
|
||
use super::window_trait::WindowPortsMethods;
|
||
use crate::parser::location_bar_input_to_url;
|
||
use crate::prefs::ServoShellPreferences;
|
||
|
||
pub struct Minibrowser {
|
||
pub context: EguiGlow,
|
||
pub event_queue: RefCell<Vec<MinibrowserEvent>>,
|
||
pub toolbar_height: Length<f32, DeviceIndependentPixel>,
|
||
|
||
/// The framebuffer object name for the widget surface we should draw to, or None if our widget
|
||
/// surface does not use a framebuffer object.
|
||
widget_surface_fbo: Option<NativeFramebuffer>,
|
||
|
||
last_update: Instant,
|
||
last_mouse_position: Option<Point2D<f32, DeviceIndependentPixel>>,
|
||
location: RefCell<String>,
|
||
|
||
/// Whether the location has been edited by the user without clicking Go.
|
||
location_dirty: Cell<bool>,
|
||
|
||
load_status: LoadStatus,
|
||
|
||
status_text: Option<String>,
|
||
}
|
||
|
||
pub enum MinibrowserEvent {
|
||
/// Go button clicked.
|
||
Go,
|
||
Back,
|
||
Forward,
|
||
Reload,
|
||
NewWebView,
|
||
}
|
||
|
||
fn truncate_with_ellipsis(input: &str, max_length: usize) -> String {
|
||
if input.chars().count() > max_length {
|
||
let truncated: String = input.chars().take(max_length.saturating_sub(1)).collect();
|
||
format!("{}…", truncated)
|
||
} else {
|
||
input.to_string()
|
||
}
|
||
}
|
||
|
||
impl Minibrowser {
|
||
pub fn new(
|
||
rendering_context: &SurfmanRenderingContext,
|
||
event_loop: &ActiveEventLoop,
|
||
initial_url: ServoUrl,
|
||
) -> Self {
|
||
let gl = unsafe {
|
||
glow::Context::from_loader_function(|s| rendering_context.get_proc_address(s))
|
||
};
|
||
|
||
// Adapted from https://github.com/emilk/egui/blob/9478e50d012c5138551c38cbee16b07bc1fcf283/crates/egui_glow/examples/pure_glow.rs
|
||
#[allow(clippy::arc_with_non_send_sync)]
|
||
let context = EguiGlow::new(event_loop, Arc::new(gl), None);
|
||
|
||
// Disable the builtin egui handlers for the Ctrl+Plus, Ctrl+Minus and Ctrl+0
|
||
// shortcuts as they don't work well with servoshell's `device-pixel-ratio` CLI argument.
|
||
context
|
||
.egui_ctx
|
||
.options_mut(|options| options.zoom_with_keyboard = false);
|
||
|
||
let widget_surface_fbo = match rendering_context.context_surface_info() {
|
||
Ok(Some(info)) => NonZeroU32::new(info.framebuffer_object).map(NativeFramebuffer),
|
||
Ok(None) => panic!("Failed to get widget surface info from surfman!"),
|
||
Err(error) => panic!("Failed to get widget surface info from surfman! {error:?}"),
|
||
};
|
||
|
||
Self {
|
||
context,
|
||
event_queue: RefCell::new(vec![]),
|
||
toolbar_height: Default::default(),
|
||
widget_surface_fbo,
|
||
last_update: Instant::now(),
|
||
last_mouse_position: None,
|
||
location: RefCell::new(initial_url.to_string()),
|
||
location_dirty: false.into(),
|
||
load_status: LoadStatus::LoadComplete,
|
||
status_text: None,
|
||
}
|
||
}
|
||
|
||
/// Preprocess the given [winit::event::WindowEvent], returning unconsumed for mouse events in
|
||
/// the Servo browser rect. This is needed because the CentralPanel we create for our webview
|
||
/// would otherwise make egui report events in that area as consumed.
|
||
pub fn on_window_event(&mut self, window: &Window, event: &WindowEvent) -> EventResponse {
|
||
let mut result = self.context.on_window_event(window, event);
|
||
result.consumed &= match event {
|
||
WindowEvent::CursorMoved { position, .. } => {
|
||
let scale = Scale::<_, DeviceIndependentPixel, _>::new(
|
||
self.context.egui_ctx.pixels_per_point(),
|
||
);
|
||
self.last_mouse_position =
|
||
Some(winit_position_to_euclid_point(*position).to_f32() / scale);
|
||
self.last_mouse_position
|
||
.map_or(false, |p| self.is_in_browser_rect(p))
|
||
},
|
||
WindowEvent::MouseInput {
|
||
state: ElementState::Pressed,
|
||
button: MouseButton::Forward,
|
||
..
|
||
} => {
|
||
self.event_queue
|
||
.borrow_mut()
|
||
.push(MinibrowserEvent::Forward);
|
||
true
|
||
},
|
||
WindowEvent::MouseInput {
|
||
state: ElementState::Pressed,
|
||
button: MouseButton::Back,
|
||
..
|
||
} => {
|
||
self.event_queue.borrow_mut().push(MinibrowserEvent::Back);
|
||
true
|
||
},
|
||
WindowEvent::MouseWheel { .. } | WindowEvent::MouseInput { .. } => self
|
||
.last_mouse_position
|
||
.map_or(false, |p| self.is_in_browser_rect(p)),
|
||
_ => true,
|
||
};
|
||
result
|
||
}
|
||
|
||
/// Return true iff the given position is in the Servo browser rect.
|
||
fn is_in_browser_rect(&self, position: Point2D<f32, DeviceIndependentPixel>) -> bool {
|
||
position.y < self.toolbar_height.get()
|
||
}
|
||
|
||
/// Create a frameless button with square sizing, as used in the toolbar.
|
||
fn toolbar_button(text: &str) -> egui::Button {
|
||
egui::Button::new(text)
|
||
.frame(false)
|
||
.min_size(Vec2 { x: 20.0, y: 20.0 })
|
||
}
|
||
|
||
/// Draws a browser tab, checking for clicks and returns an appropriate [EmbedderEvent]
|
||
/// Using a custom widget here would've been nice, but it doesn't seem as though egui
|
||
/// supports that, so we arrange multiple Widgets in a way that they look connected.
|
||
fn browser_tab(
|
||
ui: &mut egui::Ui,
|
||
label: &str,
|
||
selected: bool,
|
||
webview_id: TopLevelBrowsingContextId,
|
||
) -> Option<EmbedderEvent> {
|
||
let old_item_spacing = ui.spacing().item_spacing;
|
||
let old_visuals = ui.visuals().clone();
|
||
let active_bg_color = old_visuals.widgets.active.weak_bg_fill;
|
||
let inactive_bg_color = old_visuals.window_fill;
|
||
ui.spacing_mut().item_spacing = egui::vec2(0.0, 0.0);
|
||
|
||
let visuals = ui.visuals_mut();
|
||
// Remove the stroke so we don't see the border between the close button and the label
|
||
visuals.widgets.active.bg_stroke.width = 0.0;
|
||
visuals.widgets.hovered.bg_stroke.width = 0.0;
|
||
// Now we make sure the fill color is always the same, irrespective of state, that way
|
||
// we can make sure that both the label and close button have the same background color
|
||
visuals.widgets.noninteractive.weak_bg_fill = inactive_bg_color;
|
||
visuals.widgets.inactive.weak_bg_fill = inactive_bg_color;
|
||
visuals.widgets.hovered.weak_bg_fill = active_bg_color;
|
||
visuals.widgets.active.weak_bg_fill = active_bg_color;
|
||
visuals.selection.bg_fill = active_bg_color;
|
||
visuals.selection.stroke.color = visuals.widgets.active.fg_stroke.color;
|
||
visuals.widgets.hovered.fg_stroke.color = visuals.widgets.active.fg_stroke.color;
|
||
|
||
// Expansion would also show that they are 2 separate widgets
|
||
visuals.widgets.active.expansion = 0.0;
|
||
visuals.widgets.hovered.expansion = 0.0;
|
||
// The rounding is changed so it looks as though the 2 widgets are a single widget
|
||
// with a uniform rounding
|
||
let rounding = egui::Rounding {
|
||
ne: 0.0,
|
||
nw: 4.0,
|
||
sw: 4.0,
|
||
se: 0.0,
|
||
};
|
||
visuals.widgets.active.rounding = rounding;
|
||
visuals.widgets.hovered.rounding = rounding;
|
||
visuals.widgets.inactive.rounding = rounding;
|
||
|
||
let tab = ui.add(SelectableLabel::new(
|
||
selected,
|
||
truncate_with_ellipsis(label, 20),
|
||
));
|
||
let tab = tab.on_hover_ui(|ui| {
|
||
ui.label(label);
|
||
});
|
||
|
||
let rounding = egui::Rounding {
|
||
ne: 4.0,
|
||
nw: 0.0,
|
||
sw: 0.0,
|
||
se: 4.0,
|
||
};
|
||
let visuals = ui.visuals_mut();
|
||
visuals.widgets.active.rounding = rounding;
|
||
visuals.widgets.hovered.rounding = rounding;
|
||
visuals.widgets.inactive.rounding = rounding;
|
||
|
||
let fill_color = if selected || tab.hovered() {
|
||
active_bg_color
|
||
} else {
|
||
inactive_bg_color
|
||
};
|
||
|
||
ui.spacing_mut().item_spacing = old_item_spacing;
|
||
let close_button = ui.add(egui::Button::new("X").fill(fill_color));
|
||
*ui.visuals_mut() = old_visuals;
|
||
if close_button.clicked() || close_button.middle_clicked() || tab.middle_clicked() {
|
||
Some(EmbedderEvent::CloseWebView(webview_id))
|
||
} else if !selected && tab.clicked() {
|
||
Some(EmbedderEvent::FocusWebView(webview_id))
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
|
||
/// Update the minibrowser, but don’t paint.
|
||
/// If `servo_framebuffer_id` is given, set up a paint callback to blit its contents to our
|
||
/// CentralPanel when [`Minibrowser::paint`] is called.
|
||
pub fn update(
|
||
&mut self,
|
||
window: &Window,
|
||
webviews: &mut WebViewManager<dyn WindowPortsMethods>,
|
||
servo_framebuffer_id: Option<gl::GLuint>,
|
||
reason: &'static str,
|
||
) {
|
||
let now = Instant::now();
|
||
trace!(
|
||
"{:?} since last update ({})",
|
||
now - self.last_update,
|
||
reason
|
||
);
|
||
let Self {
|
||
context,
|
||
event_queue,
|
||
toolbar_height,
|
||
widget_surface_fbo,
|
||
last_update,
|
||
location,
|
||
location_dirty,
|
||
..
|
||
} = self;
|
||
let widget_fbo = *widget_surface_fbo;
|
||
let _duration = context.run(window, |ctx| {
|
||
// TODO: While in fullscreen add some way to mitigate the increased phishing risk
|
||
// when not displaying the URL bar: https://github.com/servo/servo/issues/32443
|
||
if window.fullscreen().is_none() {
|
||
let frame = egui::Frame::default()
|
||
.fill(ctx.style().visuals.window_fill)
|
||
.inner_margin(4.0);
|
||
TopBottomPanel::top("toolbar").frame(frame).show(ctx, |ui| {
|
||
ui.allocate_ui_with_layout(
|
||
ui.available_size(),
|
||
egui::Layout::left_to_right(egui::Align::Center),
|
||
|ui| {
|
||
if ui.add(Minibrowser::toolbar_button("⏴")).clicked() {
|
||
event_queue.borrow_mut().push(MinibrowserEvent::Back);
|
||
}
|
||
if ui.add(Minibrowser::toolbar_button("⏵")).clicked() {
|
||
event_queue.borrow_mut().push(MinibrowserEvent::Forward);
|
||
}
|
||
|
||
match self.load_status {
|
||
LoadStatus::LoadStart | LoadStatus::HeadParsed => {
|
||
if ui.add(Minibrowser::toolbar_button("X")).clicked() {
|
||
warn!("Do not support stop yet.");
|
||
}
|
||
},
|
||
LoadStatus::LoadComplete => {
|
||
if ui.add(Minibrowser::toolbar_button("↻")).clicked() {
|
||
event_queue.borrow_mut().push(MinibrowserEvent::Reload);
|
||
}
|
||
},
|
||
}
|
||
ui.add_space(2.0);
|
||
|
||
ui.allocate_ui_with_layout(
|
||
ui.available_size(),
|
||
egui::Layout::right_to_left(egui::Align::Center),
|
||
|ui| {
|
||
let location_id = egui::Id::new("location_input");
|
||
let location_field = ui.add_sized(
|
||
ui.available_size(),
|
||
egui::TextEdit::singleline(&mut *location.borrow_mut())
|
||
.id(location_id),
|
||
);
|
||
|
||
if location_field.changed() {
|
||
location_dirty.set(true);
|
||
}
|
||
if ui.input(|i| {
|
||
i.clone().consume_key(Modifiers::COMMAND, Key::L)
|
||
}) {
|
||
location_field.request_focus();
|
||
if let Some(mut state) =
|
||
TextEditState::load(ui.ctx(), location_id)
|
||
{
|
||
// Select the whole input.
|
||
state.cursor.set_char_range(Some(CCursorRange::two(
|
||
CCursor::new(0),
|
||
CCursor::new(location.borrow().len()),
|
||
)));
|
||
state.store(ui.ctx(), location_id);
|
||
}
|
||
}
|
||
if location_field.lost_focus() &&
|
||
ui.input(|i| i.clone().key_pressed(Key::Enter))
|
||
{
|
||
event_queue.borrow_mut().push(MinibrowserEvent::Go);
|
||
location_dirty.set(false);
|
||
}
|
||
},
|
||
);
|
||
},
|
||
);
|
||
});
|
||
};
|
||
|
||
let mut embedder_events = vec![];
|
||
|
||
// A simple Tab header strip
|
||
TopBottomPanel::top("tabs").show(ctx, |ui| {
|
||
ui.allocate_ui_with_layout(
|
||
ui.available_size(),
|
||
egui::Layout::left_to_right(egui::Align::Center),
|
||
|ui| {
|
||
for (webview_id, webview) in webviews.webviews().into_iter() {
|
||
let label = match (&webview.title, &webview.url) {
|
||
(Some(title), _) if !title.is_empty() => title,
|
||
(_, Some(url)) => &url.to_string(),
|
||
_ => "New Tab",
|
||
};
|
||
if let Some(event) =
|
||
Self::browser_tab(ui, label, webview.focused, webview_id)
|
||
{
|
||
location_dirty.set(false);
|
||
embedder_events.push(event);
|
||
}
|
||
}
|
||
if ui.add(Minibrowser::toolbar_button("+")).clicked() {
|
||
event_queue.borrow_mut().push(MinibrowserEvent::NewWebView);
|
||
}
|
||
},
|
||
);
|
||
});
|
||
|
||
// The toolbar height is where the Context’s available rect starts.
|
||
// For reasons that are unclear, the TopBottomPanel’s ui cursor exceeds this by one egui
|
||
// point, but the Context is correct and the TopBottomPanel is wrong.
|
||
*toolbar_height = Length::new(ctx.available_rect().min.y);
|
||
|
||
let scale =
|
||
Scale::<_, DeviceIndependentPixel, DevicePixel>::new(ctx.pixels_per_point());
|
||
let Some(focused_webview_id) = webviews.focused_webview_id() else {
|
||
return;
|
||
};
|
||
let Some(webview) = webviews.get_mut(focused_webview_id) else {
|
||
return;
|
||
};
|
||
|
||
CentralPanel::default()
|
||
.frame(Frame::none())
|
||
.show(ctx, |ui| {
|
||
let Pos2 { x, y } = ui.cursor().min;
|
||
let Vec2 {
|
||
x: width,
|
||
y: height,
|
||
} = ui.available_size();
|
||
let rect = Box2D::from_origin_and_size(
|
||
Point2D::new(x, y),
|
||
Size2D::new(width, height),
|
||
) * scale;
|
||
if rect != webview.rect {
|
||
webview.rect = rect;
|
||
embedder_events
|
||
.push(EmbedderEvent::MoveResizeWebView(focused_webview_id, rect));
|
||
}
|
||
let min = ui.cursor().min;
|
||
let size = ui.available_size();
|
||
let rect = egui::Rect::from_min_size(min, size);
|
||
ui.allocate_space(size);
|
||
|
||
let Some(servo_fbo) = servo_framebuffer_id else {
|
||
return;
|
||
};
|
||
|
||
if let Some(status_text) = &self.status_text {
|
||
egui::containers::popup::show_tooltip_at(
|
||
ctx,
|
||
ui.layer_id(),
|
||
"tooltip layer".into(),
|
||
pos2(0.0, ctx.available_rect().max.y),
|
||
|ui| ui.add(Label::new(status_text.clone()).extend()),
|
||
);
|
||
}
|
||
|
||
ui.painter().add(PaintCallback {
|
||
rect,
|
||
callback: Arc::new(CallbackFn::new(move |info, painter| {
|
||
use glow::HasContext as _;
|
||
let clip = info.viewport_in_pixels();
|
||
let x = clip.left_px as gl::GLint;
|
||
let y = clip.from_bottom_px as gl::GLint;
|
||
let width = clip.width_px as gl::GLsizei;
|
||
let height = clip.height_px as gl::GLsizei;
|
||
unsafe {
|
||
painter.gl().clear_color(0.0, 0.0, 0.0, 0.0);
|
||
painter.gl().scissor(x, y, width, height);
|
||
painter.gl().enable(gl::SCISSOR_TEST);
|
||
painter.gl().clear(gl::COLOR_BUFFER_BIT);
|
||
painter.gl().disable(gl::SCISSOR_TEST);
|
||
|
||
let servo_fbo = NonZeroU32::new(servo_fbo).map(NativeFramebuffer);
|
||
painter
|
||
.gl()
|
||
.bind_framebuffer(gl::READ_FRAMEBUFFER, servo_fbo);
|
||
painter
|
||
.gl()
|
||
.bind_framebuffer(gl::DRAW_FRAMEBUFFER, widget_fbo);
|
||
painter.gl().blit_framebuffer(
|
||
x,
|
||
y,
|
||
x + width,
|
||
y + height,
|
||
x,
|
||
y,
|
||
x + width,
|
||
y + height,
|
||
gl::COLOR_BUFFER_BIT,
|
||
gl::NEAREST,
|
||
);
|
||
painter.gl().bind_framebuffer(gl::FRAMEBUFFER, widget_fbo);
|
||
}
|
||
})),
|
||
});
|
||
});
|
||
|
||
if !embedder_events.is_empty() {
|
||
webviews.handle_window_events(embedder_events);
|
||
}
|
||
|
||
*last_update = now;
|
||
});
|
||
}
|
||
|
||
/// Paint the minibrowser, as of the last update.
|
||
pub fn paint(&mut self, window: &Window) {
|
||
unsafe {
|
||
use glow::HasContext as _;
|
||
self.context
|
||
.painter
|
||
.gl()
|
||
.bind_framebuffer(gl::FRAMEBUFFER, self.widget_surface_fbo);
|
||
}
|
||
self.context.paint(window);
|
||
}
|
||
|
||
/// Takes any outstanding events from the [Minibrowser], converting them to [EmbedderEvent] and
|
||
/// routing those to the App event queue.
|
||
pub fn queue_embedder_events_for_minibrowser_events(
|
||
&self,
|
||
browser: &WebViewManager<dyn WindowPortsMethods>,
|
||
app_event_queue: &mut Vec<EmbedderEvent>,
|
||
preferences: &ServoShellPreferences,
|
||
) {
|
||
for event in self.event_queue.borrow_mut().drain(..) {
|
||
let browser_id = browser.focused_webview_id().unwrap();
|
||
match event {
|
||
MinibrowserEvent::Go => {
|
||
let location = self.location.borrow();
|
||
let Some(url) =
|
||
location_bar_input_to_url(&location.clone(), &preferences.searchpage)
|
||
else {
|
||
warn!("failed to parse location");
|
||
break;
|
||
};
|
||
app_event_queue.push(EmbedderEvent::LoadUrl(browser_id, url));
|
||
},
|
||
MinibrowserEvent::Back => {
|
||
app_event_queue.push(EmbedderEvent::Navigation(
|
||
browser_id,
|
||
TraversalDirection::Back(1),
|
||
));
|
||
},
|
||
MinibrowserEvent::Forward => {
|
||
app_event_queue.push(EmbedderEvent::Navigation(
|
||
browser_id,
|
||
TraversalDirection::Forward(1),
|
||
));
|
||
},
|
||
MinibrowserEvent::Reload => {
|
||
let browser_id = browser.focused_webview_id().unwrap();
|
||
app_event_queue.push(EmbedderEvent::Reload(browser_id));
|
||
},
|
||
MinibrowserEvent::NewWebView => {
|
||
self.location_dirty.set(false);
|
||
let url = ServoUrl::parse("servo:newtab").unwrap();
|
||
app_event_queue.push(EmbedderEvent::NewWebView(url, WebViewId::new()));
|
||
},
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Updates the location field from the given [WebViewManager], unless the user has started
|
||
/// editing it without clicking Go, returning true iff it has changed (needing an egui update).
|
||
pub fn update_location_in_toolbar(
|
||
&mut self,
|
||
browser: &mut WebViewManager<dyn WindowPortsMethods>,
|
||
) -> bool {
|
||
// User edited without clicking Go?
|
||
if self.location_dirty.get() {
|
||
return false;
|
||
}
|
||
|
||
match browser.current_url_string() {
|
||
Some(location) if location != *self.location.get_mut() => {
|
||
self.location = RefCell::new(location.to_owned());
|
||
true
|
||
},
|
||
_ => false,
|
||
}
|
||
}
|
||
|
||
/// Updates the spinner from the given [WebViewManager], returning true iff it has changed
|
||
/// (needing an egui update).
|
||
pub fn update_spinner_in_toolbar(
|
||
&mut self,
|
||
browser: &mut WebViewManager<dyn WindowPortsMethods>,
|
||
) -> bool {
|
||
let need_update = browser.load_status() != self.load_status;
|
||
self.load_status = browser.load_status();
|
||
need_update
|
||
}
|
||
|
||
pub fn update_status_text(
|
||
&mut self,
|
||
browser: &mut WebViewManager<dyn WindowPortsMethods>,
|
||
) -> bool {
|
||
let need_update = browser.status_text() != self.status_text;
|
||
self.status_text = browser.status_text();
|
||
need_update
|
||
}
|
||
|
||
/// Updates all fields taken from the given [WebViewManager], such as the location field.
|
||
/// Returns true iff the egui needs an update.
|
||
pub fn update_webview_data(
|
||
&mut self,
|
||
browser: &mut WebViewManager<dyn WindowPortsMethods>,
|
||
) -> bool {
|
||
// Note: We must use the "bitwise OR" (|) operator here instead of "logical OR" (||)
|
||
// because logical OR would short-circuit if any of the functions return true.
|
||
// We want to ensure that all functions are called. The "bitwise OR" operator
|
||
// does not short-circuit.
|
||
self.update_location_in_toolbar(browser) |
|
||
self.update_spinner_in_toolbar(browser) |
|
||
self.update_status_text(browser)
|
||
}
|
||
}
|