Basic tab strip for the minibrowser (#33100)

This implements a simple tab system for servoshell:
- The egui part uses the built-in SelectableLabels components and
  display the full tab title on hover.
- WebView structs now hold all the state for each WebView. When we
  need "global" state, we return the focused WebView state, eg.
  for the load status since it's still global in the UI.
- New keyboard shortcut: [Cmd-or-Ctrl]+[W] to close the current tab.
- New keyboard shortcut: [Cmd-or-Ctrl]+[T] to create a new tab.
- The new tab content is loaded from the 'servo:newtab' url using a
  couple of custom protocol handlers.

Signed-off-by: webbeef <me@webbeef.org>
This commit is contained in:
webbeef 2024-08-27 13:17:33 -07:00 committed by GitHub
parent a0ff57cea1
commit 1b48bd18aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 455 additions and 72 deletions

View file

@ -7,9 +7,11 @@ 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, Color32, Frame, Key, Label, Modifiers, PaintCallback, Pos2, TopBottomPanel,
Vec2,
pos2, CentralPanel, Color32, Frame, Key, Label, Modifiers, PaintCallback, Pos2,
SelectableLabel, TopBottomPanel, Vec2,
};
use egui_glow::CallbackFn;
use egui_winit::EventResponse;
@ -61,6 +63,15 @@ pub enum MinibrowserEvent {
Reload,
}
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: &RenderingContext,
@ -216,9 +227,11 @@ impl Minibrowser {
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()),
egui::TextEdit::singleline(&mut *location.borrow_mut())
.id(location_id),
);
if location_field.changed() {
@ -228,6 +241,16 @@ impl Minibrowser {
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))
@ -242,6 +265,36 @@ impl Minibrowser {
});
};
let mut embedder_events = vec![];
// A simple Tab header strip, using egui 'SelectableLabel' elements.
// TODO: Add a way to close a tab eg. with a [x] control.
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 msg = match (webview.title.clone(), webview.url.clone()) {
(Some(title), _) => title,
(None, Some(url)) => url.to_string(),
_ => "".to_owned(),
};
let tab = ui.add(SelectableLabel::new(
webview.focused,
truncate_with_ellipsis(&msg, 20),
));
let tab = tab.on_hover_ui(|ui| {
ui.label(&msg);
});
if !webview.focused && tab.clicked() {
embedder_events.push(EmbedderEvent::FocusWebView(webview_id));
}
}
},
);
});
// The toolbar height is where the Contexts available rect starts.
// For reasons that are unclear, the TopBottomPanels ui cursor exceeds this by one egui
// point, but the Context is correct and the TopBottomPanel is wrong.
@ -255,7 +308,6 @@ impl Minibrowser {
let Some(webview) = webviews.get_mut(focused_webview_id) else {
return;
};
let mut embedder_events = vec![];
CentralPanel::default()
.frame(Frame::none())
@ -362,9 +414,9 @@ impl Minibrowser {
app_event_queue: &mut Vec<EmbedderEvent>,
) {
for event in self.event_queue.borrow_mut().drain(..) {
let browser_id = browser.focused_webview_id().unwrap();
match event {
MinibrowserEvent::Go => {
let browser_id = browser.webview_id().unwrap();
let location = self.location.borrow();
if let Some(url) = location_bar_input_to_url(&location.clone()) {
app_event_queue.push(EmbedderEvent::LoadUrl(browser_id, url));
@ -374,21 +426,19 @@ impl Minibrowser {
}
},
MinibrowserEvent::Back => {
let browser_id = browser.webview_id().unwrap();
app_event_queue.push(EmbedderEvent::Navigation(
browser_id,
TraversalDirection::Back(1),
));
},
MinibrowserEvent::Forward => {
let browser_id = browser.webview_id().unwrap();
app_event_queue.push(EmbedderEvent::Navigation(
browser_id,
TraversalDirection::Forward(1),
));
},
MinibrowserEvent::Reload => {
let browser_id = browser.webview_id().unwrap();
let browser_id = browser.focused_webview_id().unwrap();
app_event_queue.push(EmbedderEvent::Reload(browser_id));
},
}
@ -407,7 +457,7 @@ impl Minibrowser {
}
match browser.current_url_string() {
Some(location) if location != self.location.get_mut() => {
Some(location) if location != *self.location.get_mut() => {
self.location = RefCell::new(location.to_owned());
true
},