servo: Add a BluetoothDeviceSelectionRequest to the API for selecting Bluetooth devices (#43580)

This change has 2 parts:
- adding `BluetoothPickDeviceRequest`, similar to other request structs
used to communicate with the embedder.
- switch from a `Vec<String>` that expected 2 * <device count> strings
to `Vec<BluetoothDeviceDescription>` which is easier to reason about.

Testing: Manual testing with a build that has the `native-bluetooth`
feature enabled.

---------

Signed-off-by: webbeef <me@webbeef.org>
This commit is contained in:
webbeef
2026-03-25 10:45:07 -07:00
committed by GitHub
parent c698be8306
commit f38c7308b0
9 changed files with 121 additions and 93 deletions

View File

@@ -14,7 +14,7 @@ use std::thread;
use std::time::Duration;
use bitflags::bitflags;
use embedder_traits::{EmbedderMsg, EmbedderProxy};
use embedder_traits::{BluetoothDeviceDescription, EmbedderMsg, EmbedderProxy};
use log::warn;
use rand::{self, Rng};
#[cfg(not(feature = "native-bluetooth"))]
@@ -485,7 +485,7 @@ impl BluetoothManager {
return None;
}
let mut dialog_rows: Vec<String> = vec![];
let mut device_descriptions = Vec::with_capacity(devices.len());
for device in devices {
let address = device.get_address().unwrap_or_default();
let name = device.get_name().await.unwrap_or_else(|_| {
@@ -496,7 +496,7 @@ impl BluetoothManager {
};
format!("Unknown ({}...)", short)
});
dialog_rows.extend_from_slice(&[address, name]);
device_descriptions.push(BluetoothDeviceDescription { address, name });
}
let (ipc_sender, ipc_receiver) =
@@ -504,7 +504,7 @@ impl BluetoothManager {
self.embedder_proxy
.send(EmbedderMsg::GetSelectedBluetoothDevice(
webview_id,
dialog_rows,
device_descriptions,
ipc_sender,
));

View File

@@ -76,10 +76,10 @@ pub use crate::site_data_manager::{SiteData, SiteDataManager, StorageType};
pub use crate::user_content_manager::UserContentManager;
pub use crate::webview::{WebView, WebViewBuilder};
pub use crate::webview_delegate::{
AlertDialog, AllowOrDenyRequest, AuthenticationRequest, ColorPicker, ConfirmDialog,
ContextMenu, CreateNewWebViewRequest, EmbedderControl, FilePicker, InputMethodControl,
NavigationRequest, PermissionRequest, PromptDialog, SelectElement, SimpleDialog,
WebResourceLoad, WebViewDelegate,
AlertDialog, AllowOrDenyRequest, AuthenticationRequest, BluetoothDeviceSelectionRequest,
ColorPicker, ConfirmDialog, ContextMenu, CreateNewWebViewRequest, EmbedderControl, FilePicker,
InputMethodControl, NavigationRequest, PermissionRequest, PromptDialog, SelectElement,
SimpleDialog, WebResourceLoad, WebViewDelegate,
};
#[cfg(feature = "webxr")]

View File

@@ -84,8 +84,8 @@ use crate::servo_delegate::{DefaultServoDelegate, ServoDelegate, ServoError};
use crate::site_data_manager::SiteDataManager;
use crate::webview::{MINIMUM_WEBVIEW_SIZE, WebView, WebViewInner};
use crate::webview_delegate::{
AllowOrDenyRequest, AuthenticationRequest, EmbedderControl, FilePicker, NavigationRequest,
PermissionRequest, ProtocolHandlerRegistration, WebResourceLoad,
AllowOrDenyRequest, AuthenticationRequest, BluetoothDeviceSelectionRequest, EmbedderControl,
FilePicker, NavigationRequest, PermissionRequest, ProtocolHandlerRegistration, WebResourceLoad,
};
#[cfg(feature = "media-gstreamer")]
@@ -564,8 +564,7 @@ impl ServoInner {
if let Some(webview) = self.get_webview_handle(webview_id) {
webview.delegate().show_bluetooth_device_dialog(
webview,
items,
response_sender,
BluetoothDeviceSelectionRequest::new(items, response_sender),
);
}
},

View File

@@ -8,16 +8,16 @@ use std::rc::Rc;
#[cfg(feature = "gamepad")]
use embedder_traits::GamepadHapticEffectType;
use embedder_traits::{
AlertResponse, AllowOrDeny, AuthenticationResponse, ConfirmResponse, ConsoleLogLevel,
ContextMenuAction, ContextMenuElementInformation, ContextMenuItem, Cursor, EmbedderControlId,
EmbedderControlResponse, FilePickerRequest, FilterPattern, InputEventId, InputEventResult,
InputMethodType, LoadStatus, MediaSessionEvent, NewWebViewDetails, Notification,
PermissionFeature, PromptResponse, RgbColor, ScreenGeometry, SelectElementOptionOrOptgroup,
SimpleDialogRequest, TraversalId, WebResourceRequest, WebResourceResponse,
WebResourceResponseMsg,
AlertResponse, AllowOrDeny, AuthenticationResponse, BluetoothDeviceDescription,
ConfirmResponse, ConsoleLogLevel, ContextMenuAction, ContextMenuElementInformation,
ContextMenuItem, Cursor, EmbedderControlId, EmbedderControlResponse, FilePickerRequest,
FilterPattern, InputEventId, InputEventResult, InputMethodType, LoadStatus, MediaSessionEvent,
NewWebViewDetails, Notification, PermissionFeature, PromptResponse, RgbColor, ScreenGeometry,
SelectElementOptionOrOptgroup, SimpleDialogRequest, TraversalId, WebResourceRequest,
WebResourceResponse, WebResourceResponseMsg,
};
use paint_api::rendering_context::RenderingContext;
use servo_base::generic_channel::GenericSender;
use servo_base::generic_channel::{GenericSender, SendError};
use servo_base::id::PipelineId;
use servo_constellation_traits::EmbedderToConstellationMessage;
use tokio::sync::mpsc::UnboundedSender as TokioSender;
@@ -126,6 +126,39 @@ pub struct ProtocolHandlerRegistration {
pub register_or_unregister: RegisterOrUnregister,
}
/// A request to let the user chose a Bluetooth device.
pub struct BluetoothDeviceSelectionRequest {
devices: Vec<BluetoothDeviceDescription>,
responder: IpcResponder<Option<String>>,
}
impl BluetoothDeviceSelectionRequest {
pub(crate) fn new(
devices: Vec<BluetoothDeviceDescription>,
responder: GenericSender<Option<String>>,
) -> Self {
Self {
devices,
responder: IpcResponder::new(responder, None),
}
}
/// Set the device chosen by the user.
pub fn pick_device(mut self, device: &BluetoothDeviceDescription) -> Result<(), SendError> {
self.responder.send(Some(device.address.clone()))
}
/// Cancel this request.
pub fn cancel(mut self) -> Result<(), SendError> {
self.responder.send(None)
}
/// The set of devices that the user can chose from.
pub fn devices(&self) -> &Vec<BluetoothDeviceDescription> {
&self.devices
}
}
/// A request to authenticate a [`WebView`] navigation. Embedders may choose to prompt
/// the user to enter credentials or simply ignore this request (in which case credentials
/// will not be used).
@@ -943,15 +976,7 @@ pub trait WebViewDelegate {
}
/// Open dialog to select bluetooth device.
/// TODO: This API needs to be reworked to match the new model of how responses are sent.
fn show_bluetooth_device_dialog(
&self,
_webview: WebView,
_: Vec<String>,
response_sender: GenericSender<Option<String>>,
) {
let _ = response_sender.send(None);
}
fn show_bluetooth_device_dialog(&self, _webview: WebView, _: BluetoothDeviceSelectionRequest) {}
/// Request that the embedder show UI elements for form controls that are not integrated
/// into page content, such as dropdowns for `<select>` elements.

View File

@@ -424,6 +424,12 @@ impl From<ConsoleLogLevel> for log::Level {
}
}
#[derive(Clone, Deserialize, Serialize)]
pub struct BluetoothDeviceDescription {
pub address: String,
pub name: String,
}
/// Messages towards the embedder.
#[derive(Deserialize, IntoStaticStr, Serialize)]
pub enum EmbedderMsg {
@@ -483,7 +489,11 @@ pub enum EmbedderMsg {
/// A pipeline panicked. First string is the reason, second one is the backtrace.
Panic(WebViewId, String, Option<String>),
/// Open dialog to select bluetooth device.
GetSelectedBluetoothDevice(WebViewId, Vec<String>, GenericSender<Option<String>>),
GetSelectedBluetoothDevice(
WebViewId,
Vec<BluetoothDeviceDescription>,
GenericSender<Option<String>>,
),
/// Open interface to request permission specified by prompt.
PromptPermission(WebViewId, PermissionFeature, GenericSender<AllowOrDeny>),
/// Report a complete sampled profile

View File

@@ -10,12 +10,12 @@ use egui::{
};
use egui_file_dialog::{DialogState, FileDialog as EguiFileDialog};
use euclid::Length;
use log::warn;
use log::{error, warn};
use servo::{
AlertDialog, AuthenticationRequest, ColorPicker, ConfirmDialog, ContextMenu, ContextMenuItem,
DeviceIndependentPixel, EmbedderControlId, FilePicker, GenericSender, PermissionRequest,
PromptDialog, RgbColor, SelectElement, SelectElementOption, SelectElementOptionOrOptgroup,
SimpleDialog,
AlertDialog, AuthenticationRequest, BluetoothDeviceSelectionRequest, ColorPicker,
ConfirmDialog, ContextMenu, ContextMenuItem, DeviceIndependentPixel, EmbedderControlId,
FilePicker, PermissionRequest, PromptDialog, RgbColor, SelectElement, SelectElementOption,
SelectElementOptionOrOptgroup, SimpleDialog,
};
/// The minimum width of many UI elements including dialog boxes and menus,
@@ -41,9 +41,8 @@ pub enum Dialog {
request: Option<PermissionRequest>,
},
SelectDevice {
devices: Vec<String>,
request: Option<BluetoothDeviceSelectionRequest>,
selected_device_index: usize,
response_sender: GenericSender<Option<String>>,
},
SelectElement {
maybe_prompt: Option<SelectElement>,
@@ -113,14 +112,10 @@ impl Dialog {
}
}
pub fn new_device_selection_dialog(
devices: Vec<String>,
response_sender: GenericSender<Option<String>>,
) -> Self {
pub fn new_device_selection_dialog(request: BluetoothDeviceSelectionRequest) -> Self {
Dialog::SelectDevice {
devices,
request: Some(request),
selected_device_index: 0,
response_sender,
}
}
@@ -406,29 +401,36 @@ impl Dialog {
is_open
},
Dialog::SelectDevice {
devices,
request,
selected_device_index,
response_sender,
} => {
let mut is_open = true;
let modal = Modal::new("device_picker".into());
modal.show(ctx, |ui| {
if let Some(request) = request {
let mut frame = egui::Frame::default().inner_margin(10.0).begin(ui);
frame.content_ui.set_min_width(MINIMUM_UI_ELEMENT_WIDTH);
frame.content_ui.heading("Choose a Device");
frame.content_ui.add_space(10.0);
let devices = request.devices();
egui::ComboBox::from_label("")
.selected_text(&devices[*selected_device_index + 1])
.selected_text(devices[*selected_device_index].name.clone())
.show_ui(&mut frame.content_ui, |ui| {
for i in (0..devices.len() - 1).step_by(2) {
let device_name = &devices[i + 1];
ui.selectable_value(selected_device_index, i, device_name);
for (i, device) in devices.iter().enumerate() {
ui.selectable_value(
selected_device_index,
i,
device.name.clone(),
);
}
});
frame.end(ui);
} else {
error!("Unexpected: None SelectDevice while the dialog is open");
}
egui::Sides::new().show(
ui,
@@ -437,18 +439,21 @@ impl Dialog {
if ui.button("Ok").clicked() ||
ui.input(|i| i.key_pressed(egui::Key::Enter))
{
if let Err(e) = response_sender
.send(Some(devices[*selected_device_index].clone()))
{
warn!("Failed to send device selection: {}", e);
let request =
request.take().expect("non-None until dialog is closed");
let choice = &request.devices()[*selected_device_index].clone();
if let Err(error) = request.pick_device(choice) {
warn!("Failed to send device selection: {error}");
}
is_open = false;
}
if ui.button("Cancel").clicked() ||
ui.input(|i| i.key_pressed(egui::Key::Escape))
{
if let Err(e) = response_sender.send(None) {
warn!("Failed to send cancellation: {}", e);
let request =
request.take().expect("non-None until dialog is closed");
if let Err(error) = request.cancel() {
warn!("Failed to send cancellation: {error}");
}
is_open = false;
}

View File

@@ -18,9 +18,9 @@ use keyboard_types::ShortcutMatcher;
use log::{debug, info};
use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawWindowHandle};
use servo::{
AuthenticationRequest, Cursor, DeviceIndependentIntRect, DeviceIndependentPixel,
DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixel, DevicePoint, EmbedderControl,
EmbedderControlId, GenericSender, ImeEvent, InputEvent, InputEventId, InputEventResult,
AuthenticationRequest, BluetoothDeviceSelectionRequest, Cursor, DeviceIndependentIntRect,
DeviceIndependentPixel, DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixel, DevicePoint,
EmbedderControl, EmbedderControlId, ImeEvent, InputEvent, InputEventId, InputEventResult,
InputMethodControl, Key, KeyState, KeyboardEvent, Modifiers, MouseButton as ServoMouseButton,
MouseButtonAction, MouseButtonEvent, MouseLeftViewportEvent, MouseMoveEvent, NamedKey,
OffscreenRenderingContext, PermissionRequest, RenderingContext, ScreenGeometry, Theme,
@@ -1106,18 +1106,9 @@ impl PlatformWindow for HeadedWindow {
fn show_bluetooth_device_dialog(
&self,
webview_id: WebViewId,
devices: Vec<String>,
response_sender: GenericSender<Option<String>>,
request: BluetoothDeviceSelectionRequest,
) {
if devices.is_empty() {
let _ = response_sender.send(None);
return;
}
self.add_dialog(
webview_id,
Dialog::new_device_selection_dialog(devices, response_sender),
);
self.add_dialog(webview_id, Dialog::new_device_selection_dialog(request));
}
fn show_permission_dialog(&self, webview_id: WebViewId, permission_request: PermissionRequest) {

View File

@@ -19,13 +19,13 @@ use image::{DynamicImage, ImageFormat, RgbaImage};
use libc::c_char;
use log::{error, info, warn};
use servo::{
AllowOrDenyRequest, AuthenticationRequest, CSSPixel, ConsoleLogLevel, CreateNewWebViewRequest,
DeviceIntPoint, DeviceIntSize, EmbedderControl, EmbedderControlId, EventLoopWaker,
GenericSender, InputEvent, InputEventId, InputEventResult, JSValue, LoadStatus,
MediaSessionEvent, PermissionRequest, PrefValue, Preferences, ScreenshotCaptureError, Servo,
ServoDelegate, ServoError, TraversalId, UserContentManager, WebDriverCommandMsg,
WebDriverJSResult, WebDriverLoadStatus, WebDriverScriptCommand, WebDriverSenders, WebView,
WebViewDelegate, WebViewId, pref,
AllowOrDenyRequest, AuthenticationRequest, BluetoothDeviceSelectionRequest, CSSPixel,
ConsoleLogLevel, CreateNewWebViewRequest, DeviceIntPoint, DeviceIntSize, EmbedderControl,
EmbedderControlId, EventLoopWaker, GenericSender, InputEvent, InputEventId, InputEventResult,
JSValue, LoadStatus, MediaSessionEvent, PermissionRequest, PrefValue, Preferences,
ScreenshotCaptureError, Servo, ServoDelegate, ServoError, TraversalId, UserContentManager,
WebDriverCommandMsg, WebDriverJSResult, WebDriverLoadStatus, WebDriverScriptCommand,
WebDriverSenders, WebView, WebViewDelegate, WebViewId, pref,
};
use url::Url;
@@ -779,11 +779,10 @@ impl WebViewDelegate for RunningAppState {
fn show_bluetooth_device_dialog(
&self,
webview: WebView,
devices: Vec<String>,
response_sender: GenericSender<Option<String>>,
request: BluetoothDeviceSelectionRequest,
) {
self.platform_window_for_webview_id(webview.id())
.show_bluetooth_device_dialog(webview.id(), devices, response_sender);
.show_bluetooth_device_dialog(webview.id(), request);
}
fn request_permission(&self, webview: WebView, permission_request: PermissionRequest) {

View File

@@ -8,9 +8,9 @@ use std::rc::Rc;
use euclid::Scale;
use log::warn;
use servo::{
AuthenticationRequest, ConsoleLogLevel, Cursor, DeviceIndependentIntRect,
DeviceIndependentPixel, DeviceIntPoint, DeviceIntSize, DevicePixel, EmbedderControl,
EmbedderControlId, GenericSender, InputEventId, InputEventResult, MediaSessionEvent,
AuthenticationRequest, BluetoothDeviceSelectionRequest, ConsoleLogLevel, Cursor,
DeviceIndependentIntRect, DeviceIndependentPixel, DeviceIntPoint, DeviceIntSize, DevicePixel,
EmbedderControl, EmbedderControlId, InputEventId, InputEventResult, MediaSessionEvent,
PermissionRequest, RenderingContext, ScreenGeometry, WebView, WebViewBuilder, WebViewId,
};
use url::Url;
@@ -412,8 +412,7 @@ pub(crate) trait PlatformWindow {
fn show_bluetooth_device_dialog(
&self,
_: WebViewId,
_devices: Vec<String>,
_: GenericSender<Option<String>>,
_request: BluetoothDeviceSelectionRequest,
) {
}
fn show_permission_dialog(&self, _: WebViewId, _: PermissionRequest) {}