mod state;
use crate::{
actions::UnLockAction, event::WindowEvent, multi_window::window_manager::WindowManager,
user_interface::UserInterfaces,
};
use std::{
borrow::Cow,
collections::{HashMap, VecDeque},
sync::Arc,
task::Poll,
};
use crate::{clipboard::SessionLockClipboard, conversion, error::Error};
use super::DefaultStyle;
#[cfg(not(all(feature = "linux-theme-detection", target_os = "linux")))]
use iced_core::theme::Mode;
use iced_graphics::{Compositor, Shell, compositor};
use iced_core::{Size, time::Instant};
use iced_runtime::{Action, UserInterface, user_interface};
use iced_futures::{Executor, Runtime};
use iced_core::{
Event as IcedEvent,
mouse::Cursor,
theme,
window::{Event as IcedWindowEvent, Id as IcedId, RedrawRequest},
};
use iced_debug as debug;
use sessionlockev::id::Id as SessionLockId;
use sessionlockev::{DisplayWrapper, RefreshRequest};
use sessionlockev::{ReturnData, SessionLockEvent, WindowState, WindowWrapper};
use window_manager::Window;
use futures::{FutureExt, StreamExt, future::LocalBoxFuture};
use crate::{event::IcedSessionLockEvent, proxy::IcedProxy, settings::Settings};
mod window_manager;
type SessionRuntime<E, Message> = Runtime<E, IcedProxy<Action<Message>>, Action<Message>>;
use iced_program::Instance;
use iced_program::Program;
pub fn run<P>(
program: P,
settings: Settings,
compositor_settings: iced_graphics::Settings,
) -> Result<(), Error>
where
P: Program + 'static,
P::Theme: DefaultStyle,
P::Message: 'static + TryInto<UnLockAction, Error = P::Message>,
{
use futures::task;
use sessionlockev::calloop::channel::channel;
let (message_sender, message_receiver) = channel::<Action<P::Message>>();
let boot_span = debug::boot();
let proxy = IcedProxy::new(message_sender);
#[cfg(feature = "debug")]
{
let proxy = proxy.clone();
debug::on_hotpatch(move || {
proxy.send_action(Action::Reload);
});
}
let proxy_back = proxy.clone();
let mut runtime: SessionRuntime<P::Executor, P::Message> = {
let executor = P::Executor::new().map_err(Error::ExecutorCreationFailed)?;
Runtime::new(executor, proxy)
};
let (application, task) = runtime.enter(|| Instance::new(program));
if let Some(stream) = iced_runtime::task::into_stream(task) {
runtime.run(stream);
}
runtime.track(iced_futures::subscription::into_recipes(
runtime.enter(|| application.subscription().map(Action::Output)),
));
#[cfg(all(feature = "linux-theme-detection", target_os = "linux"))]
let system_theme = {
let to_mode = |color_scheme| match color_scheme {
mundy::ColorScheme::NoPreference => theme::Mode::None,
mundy::ColorScheme::Light => theme::Mode::Light,
mundy::ColorScheme::Dark => theme::Mode::Dark,
};
runtime.run(
mundy::Preferences::stream(mundy::Interest::ColorScheme)
.map(move |preferences| {
Action::System(iced_runtime::system::Action::NotifyTheme(to_mode(
preferences.color_scheme,
)))
})
.boxed(),
);
runtime
.enter(|| {
mundy::Preferences::once_blocking(
mundy::Interest::ColorScheme,
core::time::Duration::from_millis(200),
)
})
.map(|preferences| to_mode(preferences.color_scheme))
.unwrap_or_default()
};
#[cfg(not(all(feature = "linux-theme-detection", target_os = "linux")))]
let system_theme = Mode::default();
let ev: WindowState<()> = sessionlockev::WindowState::new()
.with_use_display_handle(true)
.with_connection(settings.with_connection)
.build()
.expect("Seems sessionlock is not supported");
let mut task_context = task::Context::from_waker(task::noop_waker_ref());
let context = Context::<
P,
<P as iced_program::Program>::Executor,
<P::Renderer as iced_graphics::compositor::Default>::Compositor,
>::new(
application,
compositor_settings,
runtime,
settings.fonts,
system_theme,
proxy_back,
);
let mut context_state = ContextState::Context(context);
boot_span.finish();
let mut waiting_session_lock_events = VecDeque::new();
ev.running_with_proxy(message_receiver, move |event, ev, id| {
match event {
SessionLockEvent::InitRequest => {}
SessionLockEvent::BindProvide(_, _) => {}
SessionLockEvent::RequestMessages(message) => {
waiting_session_lock_events
.push_back((id, IcedSessionLockEvent::Window(WindowEvent::from(message))));
}
SessionLockEvent::NormalDispatch => {
waiting_session_lock_events.push_back((id, IcedSessionLockEvent::NormalDispatch));
}
SessionLockEvent::UserEvent(event) => {
waiting_session_lock_events
.push_back((id, IcedSessionLockEvent::UserAction(event)));
}
_ => {}
}
loop {
let mut need_continue = false;
context_state = match std::mem::replace(&mut context_state, ContextState::None) {
ContextState::None => unreachable!("context state is taken but not returned"),
ContextState::Future(mut future) => {
tracing::debug!("poll context future");
match future.as_mut().poll(&mut task_context) {
Poll::Ready(context) => {
tracing::debug!("context future is ready");
need_continue = true;
ContextState::Context(context)
}
Poll::Pending => ContextState::Future(future),
}
}
ContextState::Context(context) => {
if let Some((session_lock_id, session_lock_event)) =
waiting_session_lock_events.pop_front()
{
need_continue = true;
let (context_state, waiting_layer_shell_event) =
context.handle_event(ev, session_lock_id, session_lock_event);
if let Some(waiting_layer_shell_event) = waiting_layer_shell_event {
waiting_session_lock_events
.push_front((session_lock_id, waiting_layer_shell_event));
}
context_state
} else {
ContextState::Context(context)
}
}
};
if !need_continue {
break;
}
}
ReturnData::None
})?;
Ok(())
}
enum ContextState<Context> {
None,
Context(Context),
Future(LocalBoxFuture<'static, Context>),
}
struct Context<P, E, C>
where
P: Program + 'static,
C: Compositor<Renderer = P::Renderer> + 'static,
E: Executor + 'static,
P::Theme: DefaultStyle,
P::Message: 'static,
{
compositor_settings: iced_graphics::Settings,
runtime: SessionRuntime<E, P::Message>,
system_theme: iced_core::theme::Mode,
fonts: Vec<Cow<'static, [u8]>>,
compositor: Option<C>,
window_manager: WindowManager<P, C>,
cached_layer_dimensions: HashMap<IcedId, (Size<u32>, f32)>,
clipboard: SessionLockClipboard,
user_interfaces: UserInterfaces<P>,
iced_events: Vec<(IcedId, IcedEvent)>,
messages: Vec<P::Message>,
proxy: IcedProxy<Action<P::Message>>,
}
impl<P, E, C> Context<P, E, C>
where
P: Program + 'static,
C: Compositor<Renderer = P::Renderer> + 'static,
E: Executor + 'static,
P::Theme: DefaultStyle,
P::Message: 'static + TryInto<UnLockAction, Error = P::Message>,
{
pub fn new(
application: Instance<P>,
compositor_settings: iced_graphics::Settings,
runtime: SessionRuntime<E, P::Message>,
fonts: Vec<Cow<'static, [u8]>>,
system_theme: iced_core::theme::Mode,
proxy: IcedProxy<Action<P::Message>>,
) -> Self {
Self {
compositor_settings,
runtime,
system_theme,
fonts,
compositor: Default::default(),
window_manager: WindowManager::new(),
cached_layer_dimensions: HashMap::new(),
clipboard: SessionLockClipboard::unconnected(),
user_interfaces: UserInterfaces::new(application),
iced_events: Default::default(),
messages: Default::default(),
proxy,
}
}
async fn create_compositor(
mut self,
window: Arc<WindowWrapper>,
display: DisplayWrapper,
) -> Self {
let shell = Shell::new(self.proxy.clone());
let mut new_compositor = C::new(self.compositor_settings, display, window.clone(), shell)
.await
.expect("Cannot create compositer");
for font in self.fonts.clone() {
new_compositor.load_font(font);
}
self.compositor = Some(new_compositor);
self.clipboard = SessionLockClipboard::connect(&window);
self
}
fn remove_compositor(&mut self) {
self.compositor = None;
self.clipboard = SessionLockClipboard::unconnected();
}
fn handle_event(
mut self,
ev: &mut WindowState<()>,
session_lock_id: Option<SessionLockId>,
session_lock_event: IcedSessionLockEvent<P::Message>,
) -> (ContextState<Self>, Option<IcedSessionLockEvent<P::Message>>) {
tracing::debug!(
"Handle sessionlock event, sessionlockev: {:?}, messages: {}",
session_lock_id,
self.messages.len(),
);
if let IcedSessionLockEvent::Window(WindowEvent::Refresh) = session_lock_event
&& self.compositor.is_none()
{
let Some(layer_shell_window) = session_lock_id.and_then(|lid| ev.get_unit_with_id(lid))
else {
tracing::error!("layer shell window not found: {:?}", session_lock_id);
return (ContextState::Context(self), None);
};
tracing::debug!("creating compositor");
let context_state = ContextState::Future(
self.create_compositor(
Arc::new(layer_shell_window.gen_wrapper()),
ev.display_wrapper(),
)
.boxed_local(),
);
return (context_state, Some(session_lock_event));
}
match session_lock_event {
IcedSessionLockEvent::Window(WindowEvent::Refresh) => {
self.handle_refresh_event(ev, session_lock_id)
}
IcedSessionLockEvent::Window(WindowEvent::Closed) => {
self.handle_closed_event(session_lock_id)
}
IcedSessionLockEvent::Window(window_event) => {
self.handle_window_event(session_lock_id, window_event)
}
IcedSessionLockEvent::UserAction(user_action) => {
self.handle_user_action(ev, user_action)
}
IcedSessionLockEvent::NormalDispatch => self.handle_normal_dispatch(ev),
}
(ContextState::Context(self), None)
}
fn handle_refresh_event(
&mut self,
ev: &mut WindowState<()>,
session_lock_id: Option<SessionLockId>,
) {
let Some(session_lock_window) = session_lock_id.and_then(|lid| ev.get_unit_with_id(lid))
else {
return;
};
let (width, height) = session_lock_window.get_size();
let scale_float = session_lock_window.scale_float();
let mut events = Vec::new();
let (iced_id, window) = if let Some((id, window)) =
self.window_manager.get_mut_alias(session_lock_window.id())
{
let window_size = window.state.window_size();
if window_size.width != width
|| window_size.height != height
|| window.state.wayland_scale_factor() != scale_float
{
let layout_span = debug::layout(id);
window.state.update_view_port(width, height, scale_float);
if let Some(ui) = self.user_interfaces.ui_mut(&id) {
ui.relayout(window.state.viewport().logical_size(), &mut window.renderer);
}
layout_span.finish();
events.push(IcedEvent::Window(IcedWindowEvent::Resized(
window.state.window_size_f32(),
)));
}
(id, window)
} else {
let wrapper = Arc::new(session_lock_window.gen_wrapper());
let iced_id = IcedId::unique();
debug::theme_changed(|| {
self.window_manager
.first()
.and_then(|window| theme::Base::palette(window.state.theme()))
});
let window = self.window_manager.insert(
iced_id,
(width, height),
scale_float,
wrapper,
self.user_interfaces.application(),
self.compositor
.as_mut()
.expect("It should have been created"),
self.system_theme,
);
self.user_interfaces.build(
iced_id,
user_interface::Cache::default(),
&mut window.renderer,
window.state.viewport().logical_size(),
);
events.push(IcedEvent::Window(IcedWindowEvent::Opened {
position: None,
size: window.state.window_size_f32(),
}));
(iced_id, window)
};
let compositor = self
.compositor
.as_mut()
.expect("The compositor should have been created");
let mut ui = self
.user_interfaces
.ui_mut(&iced_id)
.expect("Get User interface");
let cursor = if ev.is_mouse_surface(session_lock_window.id()) {
window.state.cursor()
} else {
Cursor::Unavailable
};
events.push(IcedEvent::Window(IcedWindowEvent::RedrawRequested(
Instant::now(),
)));
let draw_span = debug::draw(iced_id);
let (ui_state, statuses) = ui.update(
&events,
cursor,
&mut window.renderer,
&mut self.clipboard,
&mut self.messages,
);
let physical_size = window.state.viewport().physical_size();
if self
.cached_layer_dimensions
.get(&iced_id)
.is_none_or(|(size, scale)| {
*size != physical_size || *scale != window.state.viewport().scale_factor()
})
{
self.cached_layer_dimensions.insert(
iced_id,
(physical_size, window.state.viewport().scale_factor()),
);
compositor.configure_surface(
&mut window.surface,
physical_size.width,
physical_size.height,
);
}
for (idx, event) in events.into_iter().enumerate() {
let status = statuses
.get(idx)
.cloned()
.unwrap_or(iced_core::event::Status::Ignored);
self.runtime
.broadcast(iced_futures::subscription::Event::Interaction {
window: iced_id,
event,
status,
});
}
ui.draw(
&mut window.renderer,
window.state.theme(),
&iced_core::renderer::Style {
text_color: window.state.text_color(),
},
cursor,
);
draw_span.finish();
let session_lock_id = session_lock_window.id();
Self::handle_ui_state(ev, window, ui_state, false);
let present_span = debug::present(iced_id);
match compositor.present(
&mut window.renderer,
&mut window.surface,
window.state.viewport(),
window.state.background_color(),
|| {
ev.request_next_present(session_lock_id);
},
) {
Ok(()) => {
present_span.finish();
}
Err(error) => match error {
compositor::SurfaceError::OutOfMemory => {
panic!("{error:?}");
}
_ => {
tracing::error!("Error {error:?} when presenting surface.");
}
},
}
}
fn handle_closed_event(&mut self, session_lock_id: Option<SessionLockId>) {
let Some(session_lock_id) = session_lock_id else {
return;
};
let Some(iced_id) = self.window_manager.get_iced_id(session_lock_id) else {
return;
};
self.cached_layer_dimensions.remove(&iced_id);
self.window_manager.remove(iced_id);
self.user_interfaces.remove(&iced_id);
self.runtime
.broadcast(iced_futures::subscription::Event::Interaction {
window: iced_id,
event: IcedEvent::Window(IcedWindowEvent::Closed),
status: iced_core::event::Status::Ignored,
});
if self.window_manager.is_empty() {
self.remove_compositor();
}
}
fn handle_window_event(&mut self, session_lock_id: Option<SessionLockId>, event: WindowEvent) {
let id_and_window = if let Some(layer_shell_id) = session_lock_id {
self.window_manager.get_mut_alias(layer_shell_id)
} else {
self.window_manager.iter_mut().next()
};
let Some((iced_id, window)) = id_and_window else {
return;
};
window
.state
.update(&event, self.user_interfaces.application());
if let Some(event) = conversion::window_event(
&event,
window.state.application_scale_factor(),
window.state.modifiers(),
) {
self.iced_events.push((iced_id, event));
}
}
fn handle_ui_state(
ev: &mut WindowState<()>,
window: &mut Window<P, C>,
ui_state: user_interface::State,
unconditional_rendering: bool,
) -> bool {
match ui_state {
user_interface::State::Outdated => true,
user_interface::State::Updated {
redraw_request,
mouse_interaction,
..
} => {
if unconditional_rendering {
ev.request_refresh(window.id, RefreshRequest::NextFrame);
} else {
match redraw_request {
RedrawRequest::NextFrame => {
ev.request_refresh(window.id, RefreshRequest::NextFrame)
}
RedrawRequest::At(instant) => {
ev.request_refresh(window.id, RefreshRequest::At(instant))
}
RedrawRequest::Wait => {}
}
}
if mouse_interaction != window.mouse_interaction {
for pointer in ev.get_pointers() {
ev.append_return_data(ReturnData::RequestSetCursorShape((
conversion::mouse_interaction(mouse_interaction),
pointer,
)));
}
window.mouse_interaction = mouse_interaction;
}
false
}
}
}
fn handle_user_action(&mut self, ev: &mut WindowState<()>, action: Action<P::Message>) {
let mut should_exit = false;
run_action(
&mut self.user_interfaces,
&mut self.compositor,
action,
&mut self.messages,
&mut self.clipboard,
&mut should_exit,
&mut self.window_manager,
&mut self.system_theme,
&mut self.runtime,
ev,
);
if should_exit {
ev.append_return_data(ReturnData::RequestUnlockAndExist);
}
}
fn handle_normal_dispatch(&mut self, ev: &mut WindowState<()>) {
if self.iced_events.is_empty() && self.messages.is_empty() {
return;
}
let mut rebuilds = Vec::new();
for (iced_id, window) in self.window_manager.iter_mut() {
let interact_span = debug::interact(iced_id);
let mut window_events = vec![];
self.iced_events.retain(|(window_id, event)| {
if *window_id == iced_id {
window_events.push(event.clone());
false
} else {
true
}
});
if window_events.is_empty() && self.messages.is_empty() {
continue;
}
let (ui_state, statuses) = self
.user_interfaces
.ui_mut(&iced_id)
.expect("Get user interface")
.update(
&window_events,
window.state.cursor(),
&mut window.renderer,
&mut self.clipboard,
&mut self.messages,
);
#[cfg(feature = "unconditional-rendering")]
let unconditional_rendering = true;
#[cfg(not(feature = "unconditional-rendering"))]
let unconditional_rendering = false;
if Self::handle_ui_state(ev, window, ui_state, unconditional_rendering) {
rebuilds.push((iced_id, window));
}
for (event, status) in window_events.drain(..).zip(statuses) {
self.runtime
.broadcast(iced_futures::subscription::Event::Interaction {
window: iced_id,
event,
status,
});
}
interact_span.finish();
}
if !self.messages.is_empty() {
ev.request_refresh_all(RefreshRequest::NextFrame);
let (caches, application) = self.user_interfaces.extract_all();
let mut should_exit = false;
update(
application,
&mut self.runtime,
&mut self.messages,
&mut should_exit,
);
if should_exit {
ev.append_return_data(ReturnData::RequestUnlockAndExist);
}
for (_, window) in self.window_manager.iter_mut() {
window.state.synchronize(application);
}
debug::theme_changed(|| {
self.window_manager
.first()
.and_then(|window| theme::Base::palette(window.state.theme()))
});
for (iced_id, cache) in caches {
let Some(window) = self.window_manager.get_mut(iced_id) else {
continue;
};
self.user_interfaces.build(
iced_id,
cache,
&mut window.renderer,
window.state.viewport().logical_size(),
);
}
} else {
for (iced_id, window) in rebuilds {
if let Some(cache) = self.user_interfaces.remove(&iced_id) {
self.user_interfaces.build(
iced_id,
cache,
&mut window.renderer,
window.state.viewport().logical_size(),
);
}
}
}
}
}
#[allow(clippy::type_complexity)]
pub fn build_user_interfaces<'a, P: Program, C>(
application: &'a Instance<P>,
window_manager: &mut WindowManager<P, C>,
mut cached_user_interfaces: HashMap<iced_core::window::Id, user_interface::Cache>,
) -> HashMap<iced_core::window::Id, UserInterface<'a, P::Message, P::Theme, P::Renderer>>
where
C: Compositor<Renderer = P::Renderer>,
P::Theme: DefaultStyle,
{
cached_user_interfaces
.drain()
.filter_map(|(id, cache)| {
let window = window_manager.get_mut(id)?;
Some((
id,
build_user_interface(
application,
cache,
&mut window.renderer,
window.state.viewport().logical_size(),
id,
),
))
})
.collect()
}
fn build_user_interface<'a, A: Program>(
application: &'a Instance<A>,
cache: user_interface::Cache,
renderer: &mut A::Renderer,
size: Size,
id: iced_core::window::Id,
) -> UserInterface<'a, A::Message, A::Theme, A::Renderer>
where
A::Theme: DefaultStyle,
{
let view_span = debug::view(id);
let view = application.view(id);
view_span.finish();
let layout_span = debug::layout(id);
let user_interface = UserInterface::build(view, size, cache, renderer);
layout_span.finish();
user_interface
}
pub(crate) fn update<P: Program, E: Executor>(
application: &mut Instance<P>,
runtime: &mut SessionRuntime<E, P::Message>,
messages: &mut Vec<P::Message>,
should_exit: &mut bool,
) where
P::Theme: DefaultStyle,
P::Message: 'static + TryInto<UnLockAction, Error = P::Message>,
{
for message in messages.drain(..) {
let message = match message.try_into() {
Ok(action) => {
let _: UnLockAction = action;
*should_exit = true;
continue;
}
Err(message) => message,
};
let task = runtime.enter(|| application.update(message));
if let Some(stream) = iced_runtime::task::into_stream(task) {
runtime.run(stream);
}
}
let subscription = runtime.enter(|| application.subscription());
let recipes = iced_futures::subscription::into_recipes(subscription.map(Action::Output));
debug::subscriptions_tracked(recipes.len());
runtime.track(recipes);
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn run_action<P, C, E: Executor>(
user_interfaces: &mut UserInterfaces<P>,
compositor: &mut Option<C>,
action: Action<P::Message>,
messages: &mut Vec<P::Message>,
clipboard: &mut SessionLockClipboard,
should_exit: &mut bool,
window_manager: &mut WindowManager<P, C>,
system_theme: &mut iced_core::theme::Mode,
runtime: &mut SessionRuntime<E, P::Message>,
ev: &mut WindowState<()>,
) where
P: Program + 'static,
C: Compositor<Renderer = P::Renderer> + 'static,
P::Theme: DefaultStyle,
P::Message: 'static + TryInto<UnLockAction, Error = P::Message>,
{
use iced_core::widget::operation;
use iced_runtime::clipboard;
use iced_runtime::window::Action as WindowAction;
match action {
Action::Output(message) => match message.try_into() {
Ok(action) => {
let _: UnLockAction = action;
*should_exit = true;
}
Err(message) => messages.push(message),
},
Action::Clipboard(action) => match action {
clipboard::Action::Read { target, channel } => {
let _ = channel.send(clipboard.read(target));
}
clipboard::Action::Write { target, contents } => {
clipboard.write(target, contents);
}
},
Action::Image(action) => match action {
iced_runtime::image::Action::Allocate(handle, sender) => {
use iced_core::Renderer as _;
if let Some((_id, window)) = window_manager.iter_mut().next() {
window.renderer.allocate_image(&handle, move |allocation| {
let _ = sender.send(allocation);
});
}
}
},
Action::Widget(action) => {
let mut current_operation = Some(action);
while let Some(mut operation) = current_operation.take() {
for (id, window) in window_manager.iter_mut() {
if let Some(mut ui) = user_interfaces.ui_mut(&id) {
ui.operate(&window.renderer, operation.as_mut());
}
}
match operation.finish() {
operation::Outcome::None => {}
operation::Outcome::Some(()) => {}
operation::Outcome::Chain(next) => {
current_operation = Some(next);
}
}
}
}
Action::Window(action) => 'out: {
match action {
WindowAction::Close(_) => {
*should_exit = true;
}
WindowAction::GetSize(id, channel) => {
let Some(window) = window_manager.get(id) else {
break 'out;
};
let _ = channel.send(window.state.window_size_f32());
}
WindowAction::Screenshot(id, channel) => {
let Some(window) = window_manager.get_mut(id) else {
break 'out;
};
let Some(compositor) = compositor else {
break 'out;
};
let bytes = compositor.screenshot(
&mut window.renderer,
window.state.viewport(),
window.state.background_color(),
);
let _ = channel.send(iced_core::window::Screenshot::new(
bytes,
window.state.viewport().physical_size(),
window.state.viewport().scale_factor(),
));
}
_ => {}
}
}
Action::System(action) => match action {
iced_runtime::system::Action::GetTheme(channel) => {
let _ = channel.send(*system_theme);
}
iced_runtime::system::Action::NotifyTheme(mode) => {
if mode != *system_theme {
*system_theme = mode;
runtime.broadcast(iced_futures::subscription::Event::SystemThemeChanged(mode));
}
for (_id, window) in window_manager.iter_mut() {
window.state.update(
&WindowEvent::ThemeChanged(mode),
user_interfaces.application(),
);
}
ev.request_refresh_all(RefreshRequest::NextFrame);
}
_ => {}
},
Action::LoadFont { bytes, channel } => {
if let Some(compositor) = compositor {
compositor.load_font(bytes.clone());
let _ = channel.send(Ok(()));
}
}
Action::Reload => {
for (iced_id, window) in window_manager.iter_mut() {
if let Some(cache) = user_interfaces.remove(&iced_id) {
user_interfaces.build(
iced_id,
cache,
&mut window.renderer,
window.state.viewport().logical_size(),
);
}
}
ev.request_refresh_all(RefreshRequest::NextFrame);
}
_ => {}
}
}