From 3853e8ec9a29ccad31e57989d2714b0734f1ebcf Mon Sep 17 00:00:00 2001 From: Kavish Devar Date: Sat, 8 Nov 2025 22:01:55 +0530 Subject: [PATCH] linux-rust: add listening mode picker for airpods --- linux-rust/src/devices/airpods.rs | 2 +- linux-rust/src/devices/enums.rs | 43 ++++++ linux-rust/src/main.rs | 30 ++-- linux-rust/src/ui/airpods.rs | 237 +++++++++++++++++++++++++----- linux-rust/src/ui/nothing.rs | 2 +- linux-rust/src/ui/window.rs | 140 +++++++++++++++--- 6 files changed, 378 insertions(+), 76 deletions(-) diff --git a/linux-rust/src/devices/airpods.rs b/linux-rust/src/devices/airpods.rs index 71a533a..0d774d3 100644 --- a/linux-rust/src/devices/airpods.rs +++ b/linux-rust/src/devices/airpods.rs @@ -1,6 +1,6 @@ use crate::bluetooth::aacp::{AACPManager, ProximityKeyType, AACPEvent, AirPodsLEKeys}; use crate::bluetooth::aacp::ControlCommandIdentifiers; -// use crate::bluetooth::att::ATTManager; +use crate::bluetooth::att::ATTManager; use crate::media_controller::MediaController; use bluer::Address; use log::{debug, info, error}; diff --git a/linux-rust/src/devices/enums.rs b/linux-rust/src/devices/enums.rs index 76c9ff3..c5c5913 100644 --- a/linux-rust/src/devices/enums.rs +++ b/linux-rust/src/devices/enums.rs @@ -1,4 +1,5 @@ use std::fmt::Display; +use iced::widget::{combo_box, ComboBox}; use serde::{Deserialize, Serialize}; use crate::devices::airpods::AirPodsInformation; use crate::devices::nothing::NothingInformation; @@ -53,8 +54,50 @@ impl Display for DeviceState { #[derive(Clone, Debug)] pub struct AirPodsState { pub device_name: String, + pub noise_control_mode: AirPodsNoiseControlMode, + pub noise_control_state: combo_box::State, pub conversation_awareness_enabled: bool, pub personalized_volume_enabled: bool, + pub allow_off_mode: bool +} + +#[derive(Clone, Debug)] +pub enum AirPodsNoiseControlMode { + Off, + NoiseCancellation, + Transparency, + Adaptive +} + +impl Display for AirPodsNoiseControlMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AirPodsNoiseControlMode::Off => write!(f, "Off"), + AirPodsNoiseControlMode::NoiseCancellation => write!(f, "Noise Cancellation"), + AirPodsNoiseControlMode::Transparency => write!(f, "Transparency"), + AirPodsNoiseControlMode::Adaptive => write!(f, "Adaptive"), + } + } +} + +impl AirPodsNoiseControlMode { + pub fn from_byte(value: &u8) -> Self { + match value { + 0x01 => AirPodsNoiseControlMode::Off, + 0x02 => AirPodsNoiseControlMode::NoiseCancellation, + 0x03 => AirPodsNoiseControlMode::Transparency, + 0x04 => AirPodsNoiseControlMode::Adaptive, + _ => AirPodsNoiseControlMode::Off, + } + } + pub fn to_byte(&self) -> u8 { + match self { + AirPodsNoiseControlMode::Off => 0x01, + AirPodsNoiseControlMode::NoiseCancellation => 0x02, + AirPodsNoiseControlMode::Transparency => 0x03, + AirPodsNoiseControlMode::Adaptive => 0x04, + } + } } #[derive(Clone, Debug)] diff --git a/linux-rust/src/main.rs b/linux-rust/src/main.rs index d68e9af..09d72b1 100644 --- a/linux-rust/src/main.rs +++ b/linux-rust/src/main.rs @@ -123,18 +123,17 @@ async fn async_main( Ok(device) => { let name = device.name().await?.unwrap_or_else(|| "Unknown".to_string()); info!("Found connected AirPods: {}, initializing.", name); - let ui_tx_clone = ui_tx.clone(); - ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(device.address().to_string())).unwrap(); - let airpods_device = AirPodsDevice::new(device.address(), tray_handle.clone(), ui_tx_clone).await; + let airpods_device = AirPodsDevice::new(device.address(), tray_handle.clone(), ui_tx.clone()).await; let mut managers = device_managers.write().await; + // let dev_managers = DeviceManagers::with_both(airpods_device.aacp_manager.clone(), airpods_device.att_manager.clone()); let dev_managers = DeviceManagers::with_aacp(airpods_device.aacp_manager.clone()); managers .entry(device.address().to_string()) .or_insert(dev_managers) - .set_aacp(airpods_device.aacp_manager) - ; - drop(managers) + .set_aacp(airpods_device.aacp_manager); + drop(managers); + ui_tx.send(BluetoothUIMessage::DeviceConnected(device.address().to_string())).unwrap(); } Err(_) => { info!("No connected AirPods found."); @@ -150,16 +149,16 @@ async fn async_main( let ui_tx_clone = ui_tx.clone(); let device_managers = device_managers.clone(); tokio::spawn(async move { - ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap(); let mut managers = device_managers.write().await; match type_ { devices::enums::DeviceType::Nothing => { - let dev = devices::nothing::NothingDevice::new(device.address(), ui_tx_clone).await; + let dev = devices::nothing::NothingDevice::new(device.address(), ui_tx_clone.clone()).await; let dev_managers = DeviceManagers::with_att(dev.att_manager.clone()); managers - .entry(addr_str) + .entry(addr_str.clone()) .or_insert(dev_managers) .set_att(dev.att_manager); + ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str)).unwrap(); } _ => {} } @@ -212,14 +211,14 @@ async fn async_main( let device_managers = device_managers.clone(); tokio::spawn(async move { let mut managers = device_managers.write().await; - ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap(); - let dev = devices::nothing::NothingDevice::new(addr, ui_tx_clone).await; + let dev = devices::nothing::NothingDevice::new(addr, ui_tx_clone.clone()).await; let dev_managers = DeviceManagers::with_att(dev.att_manager.clone()); managers - .entry(addr_str) + .entry(addr_str.clone()) .or_insert(dev_managers) .set_att(dev.att_manager); drop(managers); + ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap(); }); } _ => {} @@ -236,15 +235,16 @@ async fn async_main( let ui_tx_clone = ui_tx.clone(); let device_managers = device_managers.clone(); tokio::spawn(async move { - ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap(); - let airpods_device = AirPodsDevice::new(addr, handle_clone, ui_tx_clone).await; + let airpods_device = AirPodsDevice::new(addr, handle_clone, ui_tx_clone.clone()).await; let mut managers = device_managers.write().await; + // let dev_managers = DeviceManagers::with_both(airpods_device.aacp_manager.clone(), airpods_device.att_manager.clone()); let dev_managers = DeviceManagers::with_aacp(airpods_device.aacp_manager.clone()); managers - .entry(addr_str) + .entry(addr_str.clone()) .or_insert(dev_managers) .set_aacp(airpods_device.aacp_manager); drop(managers); + ui_tx_clone.send(BluetoothUIMessage::DeviceConnected(addr_str.clone())).unwrap(); }); true })?; diff --git a/linux-rust/src/ui/airpods.rs b/linux-rust/src/ui/airpods.rs index 84bb6e4..d43af97 100644 --- a/linux-rust/src/ui/airpods.rs +++ b/linux-rust/src/ui/airpods.rs @@ -1,15 +1,17 @@ use std::collections::HashMap; use std::sync::Arc; use std::thread; -use iced::widget::{button, column, container, row, rule, text, text_input, toggler, Rule, Space}; +use iced::widget::{button, column, combo_box, container, row, rule, text, text_input, toggler, Rule, Space}; use iced::{Background, Border, Center, Color, Length, Padding, Theme}; use iced::Alignment::End; use iced::border::Radius; +use iced::overlay::menu; use iced::widget::button::Style; use iced::widget::rule::FillMode; use log::error; use tokio::runtime::Runtime; use crate::bluetooth::aacp::{AACPManager, ControlCommandIdentifiers}; +// use crate::bluetooth::att::ATTManager; use crate::devices::enums::{AirPodsState, DeviceData, DeviceInformation, DeviceState}; use crate::ui::window::Message; @@ -17,11 +19,11 @@ pub fn airpods_view<'a>( mac: &'a str, devices_list: &HashMap, state: &'a AirPodsState, - aacp_manager: Arc + aacp_manager: Arc, + // att_manager: Arc ) -> iced::widget::Container<'a, Message> { - + let mac = mac.to_string(); // order: name, noise control, press and hold config, call controls (not sure if why it might be needed, adding it just in case), audio (personalized volume, conversational awareness, adaptive audio slider), connection settings, microphone, head gestures (not adding this), off listening mode, device information - let aacp_manager_for_rename = aacp_manager.clone(); let rename_input = container( row![ @@ -57,19 +59,23 @@ pub fn airpods_view<'a>( } ) .align_x(End) - .on_input( move |new_name| { - let aacp_manager = aacp_manager_for_rename.clone(); - run_async_in_thread( - { - let new_name = new_name.clone(); - async move { - aacp_manager.send_rename_packet(&new_name).await.expect("Failed to send rename packet"); + .on_input({ + let mac = mac.clone(); + let state = state.clone(); + move|new_name| { + let aacp_manager = aacp_manager_for_rename.clone(); + run_async_in_thread( + { + let new_name = new_name.clone(); + async move { + aacp_manager.send_rename_packet(&new_name).await.expect("Failed to send rename packet"); + } } - } - ); - let mut state = state.clone(); - state.device_name = new_name.clone(); - Message::StateChanged(mac.to_string(), DeviceState::AirPods(state)) + ); + let mut state = state.clone(); + state.device_name = new_name.clone(); + Message::StateChanged(mac.to_string(), DeviceState::AirPods(state)) + } } ) ] @@ -92,6 +98,104 @@ pub fn airpods_view<'a>( } ); + let listening_mode = container(row![ + text("Listening Mode").size(16).style( + |theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().text); + style + } + ), + Space::with_width(Length::Fill), + { + let state_clone = state.clone(); + let mac = mac.clone(); + // this combo_box doesn't go really well with the design, but I am not writing my own dropdown menu for this + combo_box( + &state.noise_control_state, + "Select Listening Mode", + Some(&state.noise_control_mode.clone()), + { + let aacp_manager = aacp_manager.clone(); + move |selected_mode| { + let aacp_manager = aacp_manager.clone(); + let selected_mode_c = selected_mode.clone(); + run_async_in_thread( + async move { + aacp_manager.send_control_command( + ControlCommandIdentifiers::ListeningMode, + &[selected_mode_c.to_byte()] + ).await.expect("Failed to send Noise Control Mode command"); + } + ); + let mut state = state_clone.clone(); + state.noise_control_mode = selected_mode.clone(); + Message::StateChanged(mac.to_string(), DeviceState::AirPods(state)) + } + } + ) + .width(Length::from(200)) + .input_style( + |theme: &Theme, _status| { + text_input::Style { + background: Background::Color(theme.palette().primary.scale_alpha(0.2)), + border: Border { + width: 1.0, + color: theme.palette().text.scale_alpha(0.3), + radius: Radius::from(4.0) + }, + icon: Default::default(), + placeholder: theme.palette().text, + value: theme.palette().text, + selection: Default::default(), + } + } + ) + .padding(Padding{ + top: 5.0, + bottom: 5.0, + left: 10.0, + right: 10.0, + }) + .menu_style( + |theme: &Theme| { + menu::Style { + background: Background::Color(theme.palette().background), + border: Border { + width: 1.0, + color: theme.palette().text, + radius: Radius::from(4.0) + }, + text_color: theme.palette().text, + selected_text_color: theme.palette().text, + selected_background: Background::Color(theme.palette().primary.scale_alpha(0.3)), + } + } + ) + } + ] + .align_y(Center) + ) + .padding(Padding{ + top: 5.0, + bottom: 5.0, + left: 18.0, + right: 18.0, + }) + .style( + |theme: &Theme| { + let mut style = container::Style::default(); + style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1))); + let mut border = Border::default(); + border.color = theme.palette().primary.scale_alpha(0.5); + style.border = border.rounded(16); + style + } + ); + + let mac_audio = mac.clone(); + let mac_information = mac.clone(); + let audio_settings_col = column![ container( text("Audio Settings").size(18).style( @@ -126,20 +230,27 @@ pub fn airpods_view<'a>( ], Space::with_width(Length::Fill), toggler(state.personalized_volume_enabled) - .on_toggle(move |is_enabled| { - let aacp_manager = aacp_manager_pv.clone(); - run_async_in_thread( - async move { - aacp_manager.send_control_command( - ControlCommandIdentifiers::AdaptiveVolumeConfig, - if is_enabled { &[0x01] } else { &[0x02] } - ).await.expect("Failed to send Personalized Volume command"); - } - ); - let mut state = state.clone(); - state.personalized_volume_enabled = is_enabled; - Message::StateChanged(mac.to_string(), DeviceState::AirPods(state)) - }) + .on_toggle( + { + let mac = mac_audio.clone(); + let state = state.clone(); + move |is_enabled| { + let aacp_manager = aacp_manager_pv.clone(); + let mac = mac.clone(); + run_async_in_thread( + async move { + aacp_manager.send_control_command( + ControlCommandIdentifiers::AdaptiveVolumeConfig, + if is_enabled { &[0x01] } else { &[0x02] } + ).await.expect("Failed to send Personalized Volume command"); + } + ); + let mut state = state.clone(); + state.personalized_volume_enabled = is_enabled; + Message::StateChanged(mac, DeviceState::AirPods(state)) + } + } + ) .spacing(0) .size(20) ] @@ -182,7 +293,7 @@ pub fn airpods_view<'a>( ); let mut state = state.clone(); state.conversation_awareness_enabled = is_enabled; - Message::StateChanged(mac.to_string(), DeviceState::AirPods(state)) + Message::StateChanged(mac_audio.to_string(), DeviceState::AirPods(state)) }) .spacing(0) .size(20) @@ -211,9 +322,61 @@ pub fn airpods_view<'a>( ) ]; + let off_listening_mode_toggle = { + let aacp_manager_olm = aacp_manager.clone(); + let mac = mac.clone(); + container(row![ + column![ + text("Off Listening Mode").size(16), + text("When this is on, AIrPods listening modes will include an Off option. Loud sound levels are not reduced when listening mode is set to Off.").size(12).style( + |theme: &Theme| { + let mut style = text::Style::default(); + style.color = Some(theme.palette().text.scale_alpha(0.7)); + style + } + ) + ], + Space::with_width(Length::Fill), + toggler(state.allow_off_mode) + .on_toggle(move |is_enabled| { + let aacp_manager = aacp_manager_olm.clone(); + run_async_in_thread( + async move { + aacp_manager.send_control_command( + ControlCommandIdentifiers::AllowOffOption, + if is_enabled { &[0x01] } else { &[0x02] } + ).await.expect("Failed to send Off Listening Mode command"); + } + ); + let mut state = state.clone(); + state.allow_off_mode = is_enabled; + Message::StateChanged(mac.to_string(), DeviceState::AirPods(state)) + }) + .spacing(0) + .size(20) + ] + .align_y(Center) + ) + .padding(Padding{ + top: 5.0, + bottom: 5.0, + left: 18.0, + right: 18.0, + }) + .style( + |theme: &Theme| { + let mut style = container::Style::default(); + style.background = Some(Background::Color(theme.palette().primary.scale_alpha(0.1))); + let mut border = Border::default(); + border.color = theme.palette().primary.scale_alpha(0.5); + style.border = border.rounded(16); + style + } + ) + }; + let mut information_col = column![]; - let mac = mac.to_string(); - if let Some(device) = devices_list.get(mac.as_str()) { + if let Some(device) = devices_list.get(mac_information.as_str()) { if let Some(DeviceInformation::AirPods(ref airpods_info)) = device.information { let info_rows = column![ row![ @@ -378,7 +541,7 @@ pub fn airpods_view<'a>( ) ]; } else { - error!("Expected AirPodsInformation for device {}, got something else", mac); + error!("Expected AirPodsInformation for device {}, got something else", mac.clone()); } } @@ -386,8 +549,12 @@ pub fn airpods_view<'a>( column![ rename_input, Space::with_height(Length::from(20)), + listening_mode, + Space::with_height(Length::from(20)), audio_settings_col, - Space::with_height(Length::from(10)), + Space::with_height(Length::from(20)), + off_listening_mode_toggle, + Space::with_height(Length::from(20)), information_col ] ) diff --git a/linux-rust/src/ui/nothing.rs b/linux-rust/src/ui/nothing.rs index 5c6dd6b..4a7c196 100644 --- a/linux-rust/src/ui/nothing.rs +++ b/linux-rust/src/ui/nothing.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::sync::Arc; use iced::{Background, Border, Length, Theme}; -use iced::widget::{container, text, column, row, Space, combo_box}; +use iced::widget::{container, text, column, row, Space}; use crate::bluetooth::att::ATTManager; use crate::devices::enums::{DeviceData, DeviceInformation, NothingState}; use crate::ui::window::Message; diff --git a/linux-rust/src/ui/window.rs b/linux-rust/src/ui/window.rs index 5bba6ea..aa41199 100644 --- a/linux-rust/src/ui/window.rs +++ b/linux-rust/src/ui/window.rs @@ -12,7 +12,7 @@ use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::{Mutex, RwLock}; use crate::bluetooth::aacp::{AACPEvent, ControlCommandIdentifiers}; use crate::bluetooth::managers::DeviceManagers; -use crate::devices::enums::{AirPodsState, DeviceData, DeviceState, DeviceType, NothingAncMode, NothingState}; +use crate::devices::enums::{AirPodsNoiseControlMode, AirPodsState, DeviceData, DeviceState, DeviceType, NothingAncMode, NothingState}; use crate::ui::messages::BluetoothUIMessage; use crate::utils::{get_devices_path, get_app_settings_path, MyTheme}; use crate::ui::airpods::airpods_view; @@ -301,6 +301,29 @@ impl App { }; self.device_states.insert(mac.clone(), DeviceState::AirPods(AirPodsState { device_name, + noise_control_mode: state.control_command_status_list.iter().find_map(|status| { + if status.identifier == ControlCommandIdentifiers::ListeningMode { + status.value.get(0).map(|b| AirPodsNoiseControlMode::from_byte(b)) + } else { + None + } + }).unwrap_or(AirPodsNoiseControlMode::Transparency), + noise_control_state: combo_box::State::new( + { + let mut modes = vec![ + AirPodsNoiseControlMode::Transparency, + AirPodsNoiseControlMode::NoiseCancellation, + AirPodsNoiseControlMode::Adaptive + ]; + if state.control_command_status_list.iter().any(|status| { + status.identifier == ControlCommandIdentifiers::AllowOffOption && + matches!(status.value.as_slice(), [0x01]) + }) { + modes.insert(0, AirPodsNoiseControlMode::Off); + } + modes + } + ), conversation_awareness_enabled: state.control_command_status_list.iter().any(|status| { status.identifier == ControlCommandIdentifiers::ConversationDetectConfig && matches!(status.value.as_slice(), [0x01]) @@ -309,6 +332,10 @@ impl App { status.identifier == ControlCommandIdentifiers::AdaptiveVolumeConfig && matches!(status.value.as_slice(), [0x01]) }), + allow_off_mode: state.control_command_status_list.iter().any(|status| { + status.identifier == ControlCommandIdentifiers::AllowOffOption && + matches!(status.value.as_slice(), [0x01]) + }), })); } Some(DeviceType::Nothing) => { @@ -346,6 +373,12 @@ impl App { match event { AACPEvent::ControlCommand(status) => { match status.identifier { + ControlCommandIdentifiers::ListeningMode => { + let mode = status.value.get(0).map(|b| AirPodsNoiseControlMode::from_byte(b)).unwrap_or(AirPodsNoiseControlMode::Transparency); + if let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac) { + state.noise_control_mode = mode; + } + } ControlCommandIdentifiers::ConversationDetectConfig => { let is_enabled = match status.value.as_slice() { [0x01] => true, @@ -372,6 +405,32 @@ impl App { state.personalized_volume_enabled = is_enabled; } } + ControlCommandIdentifiers::AllowOffOption => { + let is_enabled = match status.value.as_slice() { + [0x01] => true, + [0x02] => false, + _ => { + error!("Unknown Allow Off Option value: {:?}", status.value); + false + } + }; + if let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac) { + state.allow_off_mode = is_enabled; + state.noise_control_state = combo_box::State::new( + { + let mut modes = vec![ + AirPodsNoiseControlMode::Transparency, + AirPodsNoiseControlMode::NoiseCancellation, + AirPodsNoiseControlMode::Adaptive + ]; + if is_enabled { + modes.insert(0, AirPodsNoiseControlMode::Off); + } + modes + } + ); + } + } _ => { debug!("Unhandled Control Command Status: {:?}", status); } @@ -449,7 +508,39 @@ impl App { Task::none() } Message::StateChanged(mac, state) => { - self.device_states.insert(mac, state); + self.device_states.insert(mac.clone(), state); + // if airpods, update the noise control state combo box based on allow off mode + let type_ = { + let devices_json = std::fs::read_to_string(get_devices_path()).unwrap_or_else(|e| { + error!("Failed to read devices file: {}", e); + "{}".to_string() + }); + let devices_list: HashMap = serde_json::from_str(&devices_json).unwrap_or_else(|e| { + error!("Deserialization failed: {}", e); + HashMap::new() + }); + devices_list.get(&mac).map(|d| d.type_.clone()) + }; + match type_ { + Some(DeviceType::AirPods) => { + if let Some(DeviceState::AirPods(state)) = self.device_states.get_mut(&mac) { + state.noise_control_state = combo_box::State::new( + { + let mut modes = vec![ + AirPodsNoiseControlMode::Transparency, + AirPodsNoiseControlMode::NoiseCancellation, + AirPodsNoiseControlMode::Adaptive + ]; + if state.allow_off_mode { + modes.insert(0, AirPodsNoiseControlMode::Off); + } + modes + } + ); + } + } + _ => {} + } Task::none() } } @@ -622,33 +713,34 @@ impl App { debug!("Rendering device view for {}: type={:?}, state={:?}", id, device_type, device_state); match device_type { Some(DeviceType::AirPods) => { - if let Some(DeviceState::AirPods(state)) = device_state { - if let Some(device_managers) = device_managers.get(id) { - if let Some(aacp_manager) = device_managers.get_aacp() { - airpods_view(id, &devices_list, state, aacp_manager.clone()) - } else { - error!("No AACP manager found for AirPods device {}", id); - container( - text("No valid AACP manager found for this AirPods device").size(16) - ) - .center_x(Length::Fill) - .center_y(Length::Fill) + let view = device_state.as_ref().and_then(|state| { + match state { + DeviceState::AirPods(state) => { + device_managers.get(id).and_then(|managers| { + managers.get_aacp().and_then(|aacp_manager| { + // managers.get_att().map(|att_manager| { + Some(airpods_view( + id, + &devices_list, + state, + aacp_manager.clone() + ), + // att_manager.clone(), + ) + // }) + }) + }) } - } else { - error!("No manager found for AirPods device {}", id); - container( - text("No manager found for this AirPods device").size(16) - ) - .center_x(Length::Fill) - .center_y(Length::Fill) + _ => None, } - } else { + }).unwrap_or_else(|| { container( - text("No state available for this AirPods device").size(16) + text("Required managers or state not available for this AirPods device").size(16) ) .center_x(Length::Fill) .center_y(Length::Fill) - } + }); + view } Some(DeviceType::Nothing) => { if let Some(DeviceState::Nothing(state)) = device_state { @@ -725,7 +817,7 @@ impl App { border: Border { width: 1.0, color: theme.palette().text, - radius: Radius::from(8.0) + radius: Radius::from(4.0) }, text_color: theme.palette().text, selected_text_color: theme.palette().text,