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

@@ -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| {
let mut frame = egui::Frame::default().inner_margin(10.0).begin(ui);
frame.content_ui.set_min_width(MINIMUM_UI_ELEMENT_WIDTH);
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);
frame.content_ui.heading("Choose a Device");
frame.content_ui.add_space(10.0);
egui::ComboBox::from_label("")
.selected_text(&devices[*selected_device_index + 1])
.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);
}
});
let devices = request.devices();
egui::ComboBox::from_label("")
.selected_text(devices[*selected_device_index].name.clone())
.show_ui(&mut frame.content_ui, |ui| {
for (i, device) in devices.iter().enumerate() {
ui.selectable_value(
selected_device_index,
i,
device.name.clone(),
);
}
});
frame.end(ui);
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) {}