#![allow(unsafe_code)]
#![allow(dead_code)]
use anyhow::{Context as _, Result};
use futures::channel::oneshot;
use gpui::{
self, AtlasKey, AtlasTile, Capslock, DispatchEventResult, GpuSpecs, Modifiers, PlatformAtlas,
PlatformDisplay, PlatformInputHandler, PlatformWindow, PromptButton, PromptLevel,
RequestFrameOptions, WindowBackgroundAppearance, WindowBounds, WindowControlArea,
};
use gpui_wgpu::{WgpuContext, WgpuRenderer, WgpuSurfaceConfig};
use parking_lot::Mutex;
use raw_window_handle::{
AndroidDisplayHandle, AndroidNdkWindowHandle, HasDisplayHandle, HasWindowHandle,
RawDisplayHandle, RawWindowHandle,
};
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use super::{AndroidKeyEvent, Bounds, DevicePixels, Pixels, Point, Size, TouchPoint};
use crate::momentum::{MomentumScroller, VelocityTracker};
struct MomentumState {
velocity_tracker: VelocityTracker,
scroller: MomentumScroller,
pending_scroll_dx: f32,
pending_scroll_dy: f32,
pending_scroll_pos_x: f32,
pending_scroll_pos_y: f32,
has_pending_scroll: bool,
pending_scroll_phase: gpui::TouchPhase,
}
#[repr(C)]
pub struct ANativeWindow {
_priv: [u8; 0],
}
unsafe extern "C" {
fn ANativeWindow_acquire(window: *mut ANativeWindow);
fn ANativeWindow_release(window: *mut ANativeWindow);
fn ANativeWindow_getWidth(window: *mut ANativeWindow) -> i32;
fn ANativeWindow_getHeight(window: *mut ANativeWindow) -> i32;
}
pub type RequestFrameCallback = Box<dyn FnMut() + Send + 'static>;
pub type TouchCallback = Box<dyn FnMut(TouchPoint) + Send + 'static>;
pub type ActiveStatusCallback = Box<dyn FnMut(bool) + Send + 'static>;
pub type KeyCallback = Box<dyn FnMut(AndroidKeyEvent) + Send + 'static>;
pub type ResizeCallback = Box<dyn FnMut(Size<DevicePixels>, f32) + Send + 'static>;
pub type CloseCallback = Box<dyn FnOnce() + Send + 'static>;
pub type AppearanceCallback = Box<dyn FnMut(WindowAppearance) + Send + 'static>;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
pub enum WindowAppearance {
#[default]
Light,
Dark,
HighContrastLight,
HighContrastDark,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SafeAreaInsets {
pub top: f32,
pub bottom: f32,
pub left: f32,
pub right: f32,
}
impl SafeAreaInsets {
pub fn to_logical(&self, scale_factor: f32) -> SafeAreaInsets {
SafeAreaInsets {
top: self.top / scale_factor,
bottom: self.bottom / scale_factor,
left: self.left / scale_factor,
right: self.right / scale_factor,
}
}
}
struct WindowState {
native_window: *mut ANativeWindow,
gpu_context: Option<WgpuContext>,
renderer: Option<WgpuRenderer>,
width: i32,
height: i32,
scale_factor: f32,
safe_area_insets: SafeAreaInsets,
appearance: WindowAppearance,
is_active: bool,
transparent: bool,
request_frame_callback: Option<RequestFrameCallback>,
touch_callback: Option<TouchCallback>,
key_callback: Option<KeyCallback>,
resize_callback: Option<ResizeCallback>,
close_callback: Option<CloseCallback>,
appearance_callback: Option<AppearanceCallback>,
active_status_callback: Option<ActiveStatusCallback>,
}
unsafe impl Send for WindowState {}
unsafe impl Sync for WindowState {}
pub struct AndroidWindow {
state: Arc<Mutex<WindowState>>,
id: u64,
}
unsafe impl Send for AndroidWindow {}
unsafe impl Sync for AndroidWindow {}
impl AndroidWindow {
pub unsafe fn new(
native_window: *mut ANativeWindow,
gpu_context: &mut Option<WgpuContext>,
scale_factor: f32,
transparent: bool,
) -> Result<Arc<Self>> {
anyhow::ensure!(!native_window.is_null(), "ANativeWindow must not be null");
unsafe { ANativeWindow_acquire(native_window) };
let width = unsafe { ANativeWindow_getWidth(native_window) };
let height = unsafe { ANativeWindow_getHeight(native_window) };
log::info!(
"AndroidWindow::new — {}×{} scale={:.1}",
width,
height,
scale_factor
);
let renderer = unsafe {
Self::create_renderer(native_window, gpu_context, width, height, transparent)
}
.context("failed to create gpui_wgpu renderer")?;
let id = native_window as u64;
let state = Arc::new(Mutex::new(WindowState {
native_window,
gpu_context: gpu_context.take(),
renderer: Some(renderer),
width,
height,
scale_factor,
safe_area_insets: SafeAreaInsets::default(),
appearance: WindowAppearance::Light,
is_active: true,
transparent,
request_frame_callback: None,
touch_callback: None,
key_callback: None,
resize_callback: None,
close_callback: None,
appearance_callback: None,
active_status_callback: None,
}));
Ok(Arc::new(Self { state, id }))
}
pub fn headless(width: i32, height: i32, scale_factor: f32) -> Arc<Self> {
let state = Arc::new(Mutex::new(WindowState {
native_window: std::ptr::null_mut(),
gpu_context: None,
renderer: None,
width,
height,
scale_factor,
safe_area_insets: SafeAreaInsets::default(),
appearance: WindowAppearance::Light,
is_active: false,
transparent: false,
request_frame_callback: None,
touch_callback: None,
key_callback: None,
resize_callback: None,
close_callback: None,
appearance_callback: None,
active_status_callback: None,
}));
Arc::new(Self {
state,
id: ((width as u64) << 32) | (height as u64),
})
}
pub unsafe fn init_window(
&self,
native_window: *mut ANativeWindow,
gpu_context: &mut Option<WgpuContext>,
) -> Result<()> {
anyhow::ensure!(!native_window.is_null(), "ANativeWindow must not be null");
unsafe { ANativeWindow_acquire(native_window) };
let width = unsafe { ANativeWindow_getWidth(native_window) };
let height = unsafe { ANativeWindow_getHeight(native_window) };
let mut state = self.state.lock();
if !state.native_window.is_null() {
unsafe { ANativeWindow_release(state.native_window) };
}
state.native_window = native_window;
state.width = width;
state.height = height;
let transparent = state.transparent;
let renderer = if state.gpu_context.is_some() {
unsafe {
Self::create_renderer(
native_window,
&mut state.gpu_context,
width,
height,
transparent,
)
}?
} else {
let r = unsafe {
Self::create_renderer(native_window, gpu_context, width, height, transparent)
}?;
state.gpu_context = gpu_context.take();
r
};
state.renderer = Some(renderer);
state.is_active = true;
log::info!("AndroidWindow::init_window — {}×{}", width, height);
Ok(())
}
pub fn term_window(&self) {
let mut state = self.state.lock();
if let Some(mut renderer) = state.renderer.take() {
renderer.destroy();
log::info!("AndroidWindow::term_window — renderer destroyed");
}
if !state.native_window.is_null() {
unsafe { ANativeWindow_release(state.native_window) };
state.native_window = std::ptr::null_mut();
}
state.is_active = false;
if let Some(cb) = state.close_callback.take() {
cb();
}
}
pub fn handle_resize(&self) {
let mut state = self.state.lock();
if state.native_window.is_null() {
return;
}
let new_w = unsafe { ANativeWindow_getWidth(state.native_window) };
let new_h = unsafe { ANativeWindow_getHeight(state.native_window) };
if new_w == state.width && new_h == state.height {
return;
}
log::debug!(
"AndroidWindow resize: {}×{} → {}×{}",
state.width,
state.height,
new_w,
new_h
);
state.width = new_w;
state.height = new_h;
if let Some(renderer) = state.renderer.as_mut() {
renderer.update_drawable_size(gpui::size(
gpui::DevicePixels(new_w),
gpui::DevicePixels(new_h),
));
}
let scale = state.scale_factor;
if let Some(cb) = state.resize_callback.as_mut() {
cb(
Size {
width: DevicePixels(new_w),
height: DevicePixels(new_h),
},
scale,
);
}
}
pub fn draw(&self, scene: &gpui::Scene) {
let mut state = self.state.lock();
if let Some(renderer) = state.renderer.as_mut() {
renderer.draw(scene);
}
}
pub fn request_frame(&self) {
let cb = {
let mut state = self.state.lock();
state.request_frame_callback.take()
};
if let Some(mut cb) = cb {
cb();
let mut state = self.state.lock();
if state.request_frame_callback.is_none() {
state.request_frame_callback = Some(cb);
}
}
}
pub fn handle_touch(&self, point: TouchPoint) {
let cb = {
let mut state = self.state.lock();
state.touch_callback.take()
};
if let Some(mut cb) = cb {
let scale = self.scale_factor();
log::debug!(
"handle_touch: id={} action={} phys=({:.0},{:.0}) logical=({:.0},{:.0}) scale={:.1}",
point.id, point.action, point.x, point.y,
point.x / scale, point.y / scale, scale,
);
cb(point);
let mut state = self.state.lock();
if state.touch_callback.is_none() {
state.touch_callback = Some(cb);
}
} else {
log::warn!(
"handle_touch: NO touch_callback registered — touch dropped (id={} action={})",
point.id,
point.action,
);
}
}
pub fn handle_key_event(&self, event: AndroidKeyEvent) {
let cb = {
let mut state = self.state.lock();
state.key_callback.take()
};
if let Some(mut cb) = cb {
cb(event);
let mut state = self.state.lock();
if state.key_callback.is_none() {
state.key_callback = Some(cb);
}
}
}
pub fn set_appearance(&self, appearance: WindowAppearance) {
let mut state = self.state.lock();
if state.appearance == appearance {
return;
}
state.appearance = appearance;
if let Some(cb) = state.appearance_callback.as_mut() {
cb(appearance);
}
}
pub fn appearance(&self) -> WindowAppearance {
self.state.lock().appearance
}
pub fn set_active(&self, active: bool) {
let mut state = self.state.lock();
if state.is_active == active {
return;
}
state.is_active = active;
if let Some(cb) = state.active_status_callback.as_mut() {
cb(active);
}
}
pub fn set_transparent(&self, transparent: bool) {
let mut state = self.state.lock();
if state.transparent == transparent {
return;
}
state.transparent = transparent;
if let Some(renderer) = state.renderer.as_mut() {
renderer.update_transparency(transparent);
}
}
pub fn physical_size(&self) -> Size<DevicePixels> {
let state = self.state.lock();
Size {
width: DevicePixels(state.width),
height: DevicePixels(state.height),
}
}
pub fn logical_size(&self) -> Size<Pixels> {
let state = self.state.lock();
Size {
width: Pixels(state.width as f32 / state.scale_factor),
height: Pixels(state.height as f32 / state.scale_factor),
}
}
pub fn bounds(&self) -> Bounds<DevicePixels> {
Bounds {
origin: Point {
x: DevicePixels(0),
y: DevicePixels(0),
},
size: self.physical_size(),
}
}
pub fn scale_factor(&self) -> f32 {
self.state.lock().scale_factor
}
pub fn safe_area_insets(&self) -> SafeAreaInsets {
self.state.lock().safe_area_insets
}
pub fn safe_area_insets_logical(&self) -> SafeAreaInsets {
let state = self.state.lock();
state.safe_area_insets.to_logical(state.scale_factor)
}
pub fn update_safe_area_from_content_rect(
&self,
content_left: i32,
content_top: i32,
content_right: i32,
content_bottom: i32,
) {
let mut state = self.state.lock();
let insets = SafeAreaInsets {
top: content_top as f32,
bottom: (state.height - content_bottom).max(0) as f32,
left: content_left as f32,
right: (state.width - content_right).max(0) as f32,
};
log::info!(
"safe_area_insets updated: top={:.0} bottom={:.0} left={:.0} right={:.0} (physical px)",
insets.top,
insets.bottom,
insets.left,
insets.right,
);
state.safe_area_insets = insets;
}
pub fn has_surface(&self) -> bool {
self.state.lock().renderer.is_some()
}
pub fn sprite_atlas(&self) -> Option<Arc<dyn PlatformAtlas>> {
let state = self.state.lock();
state
.renderer
.as_ref()
.map(|r| r.sprite_atlas().clone() as Arc<dyn PlatformAtlas>)
}
pub fn gpu_specs(&self) -> Option<GpuSpecs> {
let state = self.state.lock();
state.renderer.as_ref().map(|r| r.gpu_specs())
}
pub fn is_active(&self) -> bool {
self.state.lock().is_active
}
pub fn id(&self) -> u64 {
self.id
}
pub fn on_request_frame<F>(&self, cb: F)
where
F: FnMut() + Send + 'static,
{
self.state.lock().request_frame_callback = Some(Box::new(cb));
}
pub fn on_touch<F>(&self, cb: F)
where
F: FnMut(TouchPoint) + Send + 'static,
{
self.state.lock().touch_callback = Some(Box::new(cb));
}
pub fn on_key_event<F>(&self, cb: F)
where
F: FnMut(AndroidKeyEvent) + Send + 'static,
{
self.state.lock().key_callback = Some(Box::new(cb));
}
pub fn on_resize<F>(&self, cb: F)
where
F: FnMut(Size<DevicePixels>, f32) + Send + 'static,
{
self.state.lock().resize_callback = Some(Box::new(cb));
}
pub fn on_close<F>(&self, cb: F)
where
F: FnOnce() + Send + 'static,
{
self.state.lock().close_callback = Some(Box::new(cb));
}
pub fn on_appearance_changed<F>(&self, cb: F)
where
F: FnMut(WindowAppearance) + Send + 'static,
{
self.state.lock().appearance_callback = Some(Box::new(cb));
}
pub fn on_active_status_change<F>(&self, cb: F)
where
F: FnMut(bool) + Send + 'static,
{
self.state.lock().active_status_callback = Some(Box::new(cb));
}
pub fn supports_subpixel_aa(&self) -> bool {
self.state
.lock()
.renderer
.as_ref()
.map(|r| r.supports_dual_source_blending())
.unwrap_or(false)
}
unsafe fn create_renderer(
native_window: *mut ANativeWindow,
gpu_context: &mut Option<WgpuContext>,
width: i32,
height: i32,
transparent: bool,
) -> Result<WgpuRenderer> {
struct RawWindow {
window: RawWindowHandle,
display: RawDisplayHandle,
}
impl HasWindowHandle for RawWindow {
fn window_handle(
&self,
) -> Result<raw_window_handle::WindowHandle<'_>, raw_window_handle::HandleError>
{
Ok(unsafe { raw_window_handle::WindowHandle::borrow_raw(self.window) })
}
}
impl HasDisplayHandle for RawWindow {
fn display_handle(
&self,
) -> Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError>
{
Ok(unsafe { raw_window_handle::DisplayHandle::borrow_raw(self.display) })
}
}
let raw = RawWindow {
window: RawWindowHandle::AndroidNdk(AndroidNdkWindowHandle::new(
std::ptr::NonNull::new(native_window as *mut std::ffi::c_void)
.expect("native_window is non-null"),
)),
display: RawDisplayHandle::Android(AndroidDisplayHandle::new()),
};
let config = WgpuSurfaceConfig {
size: gpui::size(gpui::DevicePixels(width), gpui::DevicePixels(height)),
transparent,
};
WgpuRenderer::new(gpu_context, &raw, config, None)
}
}
impl Drop for AndroidWindow {
fn drop(&mut self) {
let mut state = self.state.lock();
if let Some(mut renderer) = state.renderer.take() {
renderer.destroy();
}
if !state.native_window.is_null() {
unsafe { ANativeWindow_release(state.native_window) };
state.native_window = std::ptr::null_mut();
}
}
}
pub struct AndroidPlatformWindow {
window: Arc<AndroidWindow>,
display: Option<Rc<dyn PlatformDisplay>>,
input_handler: Option<PlatformInputHandler>,
input_callback: Option<Box<dyn FnMut(gpui::PlatformInput) -> DispatchEventResult>>,
active_status_callback: Option<Box<dyn FnMut(bool)>>,
hover_status_callback: Option<Box<dyn FnMut(bool)>>,
resize_callback: Option<Box<dyn FnMut(gpui::Size<gpui::Pixels>, f32)>>,
moved_callback: Option<Box<dyn FnMut()>>,
should_close_callback: Option<Box<dyn FnMut() -> bool>>,
close_callback: Option<Box<dyn FnOnce()>>,
request_frame_callback: Option<Box<dyn FnMut(RequestFrameOptions)>>,
appearance_callback: Option<Box<dyn FnMut()>>,
hit_test_callback: Option<Box<dyn FnMut() -> Option<WindowControlArea>>>,
title: String,
momentum: Arc<Mutex<MomentumState>>,
momentum_input_cb:
Arc<Mutex<Box<dyn FnMut(gpui::PlatformInput) -> DispatchEventResult + Send>>>,
}
impl AndroidPlatformWindow {
pub fn new(window: Arc<AndroidWindow>, display: Option<Rc<dyn PlatformDisplay>>) -> Self {
let noop_input_cb: Box<dyn FnMut(gpui::PlatformInput) -> DispatchEventResult + Send> =
Box::new(|_| DispatchEventResult::default());
Self {
window,
display,
input_handler: None,
input_callback: None,
active_status_callback: None,
hover_status_callback: None,
resize_callback: None,
moved_callback: None,
should_close_callback: None,
close_callback: None,
request_frame_callback: None,
appearance_callback: None,
hit_test_callback: None,
title: String::new(),
momentum: Arc::new(Mutex::new(MomentumState {
velocity_tracker: VelocityTracker::new(),
scroller: MomentumScroller::new(),
pending_scroll_dx: 0.0,
pending_scroll_dy: 0.0,
pending_scroll_pos_x: 0.0,
pending_scroll_pos_y: 0.0,
has_pending_scroll: false,
pending_scroll_phase: gpui::TouchPhase::Moved,
})),
momentum_input_cb: Arc::new(Mutex::new(noop_input_cb)),
}
}
pub fn inner(&self) -> &Arc<AndroidWindow> {
&self.window
}
}
impl HasWindowHandle for AndroidPlatformWindow {
fn window_handle(
&self,
) -> std::result::Result<raw_window_handle::WindowHandle<'_>, raw_window_handle::HandleError>
{
let state = self.window.state.lock();
if state.native_window.is_null() {
return Err(raw_window_handle::HandleError::Unavailable);
}
let ndk_handle = AndroidNdkWindowHandle::new(
std::ptr::NonNull::new(state.native_window as *mut std::ffi::c_void)
.expect("checked non-null above"),
);
let raw = RawWindowHandle::AndroidNdk(ndk_handle);
Ok(unsafe { raw_window_handle::WindowHandle::borrow_raw(raw) })
}
}
impl HasDisplayHandle for AndroidPlatformWindow {
fn display_handle(
&self,
) -> std::result::Result<raw_window_handle::DisplayHandle<'_>, raw_window_handle::HandleError>
{
let raw = RawDisplayHandle::Android(AndroidDisplayHandle::new());
Ok(unsafe { raw_window_handle::DisplayHandle::borrow_raw(raw) })
}
}
impl PlatformWindow for AndroidPlatformWindow {
fn bounds(&self) -> gpui::Bounds<gpui::Pixels> {
let state = self.window.state.lock();
let w = state.width as f32 / state.scale_factor;
let h = state.height as f32 / state.scale_factor;
gpui::Bounds {
origin: gpui::point(gpui::px(0.0), gpui::px(0.0)),
size: gpui::size(gpui::px(w), gpui::px(h)),
}
}
fn is_maximized(&self) -> bool {
true
}
fn window_bounds(&self) -> WindowBounds {
WindowBounds::Fullscreen(self.bounds())
}
fn content_size(&self) -> gpui::Size<gpui::Pixels> {
self.bounds().size
}
fn resize(&mut self, _size: gpui::Size<gpui::Pixels>) {
}
fn scale_factor(&self) -> f32 {
self.window.scale_factor()
}
fn appearance(&self) -> gpui::WindowAppearance {
let local_appearance = self.window.appearance();
match local_appearance {
WindowAppearance::Dark => gpui::WindowAppearance::Dark,
WindowAppearance::HighContrastDark => gpui::WindowAppearance::VibrantDark,
WindowAppearance::Light | WindowAppearance::HighContrastLight => {
gpui::WindowAppearance::Light
}
}
}
fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
self.display.clone()
}
fn mouse_position(&self) -> gpui::Point<gpui::Pixels> {
gpui::Point::default()
}
fn modifiers(&self) -> Modifiers {
Modifiers::default()
}
fn capslock(&self) -> Capslock {
Capslock::default()
}
fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
self.input_handler = Some(input_handler);
}
fn take_input_handler(&mut self) -> Option<PlatformInputHandler> {
self.input_handler.take()
}
fn prompt(
&self,
_level: PromptLevel,
_msg: &str,
_detail: Option<&str>,
_answers: &[PromptButton],
) -> Option<oneshot::Receiver<usize>> {
None
}
fn activate(&self) {
}
fn is_active(&self) -> bool {
self.window.is_active()
}
fn is_hovered(&self) -> bool {
self.window.is_active()
}
fn background_appearance(&self) -> WindowBackgroundAppearance {
WindowBackgroundAppearance::Opaque
}
fn set_title(&mut self, title: &str) {
self.title = title.to_string();
log::debug!("AndroidPlatformWindow::set_title({title})");
}
fn set_background_appearance(&self, _background: WindowBackgroundAppearance) {
}
fn minimize(&self) {
log::debug!("AndroidPlatformWindow::minimize — no-op on Android");
}
fn zoom(&self) {
}
fn toggle_fullscreen(&self) {
}
fn is_fullscreen(&self) -> bool {
true
}
fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>) {
let send_callback: Box<dyn FnMut(RequestFrameOptions) + Send> =
unsafe { std::mem::transmute(callback) };
let send_callback = Mutex::new(send_callback);
let momentum = Arc::clone(&self.momentum);
let input_cb = Arc::clone(&self.momentum_input_cb);
self.window.on_request_frame(move || {
{
let mut ms = momentum.lock();
if ms.has_pending_scroll {
let dx = ms.pending_scroll_dx;
let dy = ms.pending_scroll_dy;
let pos_x = ms.pending_scroll_pos_x;
let pos_y = ms.pending_scroll_pos_y;
let phase = ms.pending_scroll_phase;
ms.pending_scroll_dx = 0.0;
ms.pending_scroll_dy = 0.0;
ms.has_pending_scroll = false;
drop(ms);
let position = gpui::point(gpui::px(pos_x), gpui::px(pos_y));
if let Some(mut guard) = input_cb.try_lock() {
let _ = guard(gpui::PlatformInput::ScrollWheel(gpui::ScrollWheelEvent {
position,
delta: gpui::ScrollDelta::Pixels(gpui::point(
gpui::px(dx),
gpui::px(dy),
)),
modifiers: gpui::Modifiers::default(),
touch_phase: phase,
}));
}
} else if ms.scroller.is_active() {
if let Some(delta) = ms.scroller.step() {
let position =
gpui::point(gpui::px(delta.position_x), gpui::px(delta.position_y));
drop(ms);
if let Some(mut guard) = input_cb.try_lock() {
let _ =
guard(gpui::PlatformInput::ScrollWheel(gpui::ScrollWheelEvent {
position,
delta: gpui::ScrollDelta::Pixels(gpui::point(
gpui::px(delta.dx),
gpui::px(delta.dy),
)),
modifiers: gpui::Modifiers::default(),
touch_phase: gpui::TouchPhase::Moved,
}));
}
} else {
drop(ms);
if let Some(mut guard) = input_cb.try_lock() {
let _ =
guard(gpui::PlatformInput::ScrollWheel(gpui::ScrollWheelEvent {
position: gpui::point(gpui::px(0.0), gpui::px(0.0)),
delta: gpui::ScrollDelta::Pixels(gpui::point(
gpui::px(0.0),
gpui::px(0.0),
)),
modifiers: gpui::Modifiers::default(),
touch_phase: gpui::TouchPhase::Ended,
}));
}
}
}
}
let mut cb = send_callback.lock();
cb(RequestFrameOptions {
require_presentation: true,
force_render: false,
});
});
}
fn on_input(&self, callback: Box<dyn FnMut(gpui::PlatformInput) -> DispatchEventResult>) {
let send_callback: Box<dyn FnMut(gpui::PlatformInput) -> DispatchEventResult + Send> =
unsafe { std::mem::transmute(callback) };
let input_cb = Arc::new(Mutex::new(send_callback));
*self.momentum_input_cb.lock() = {
let cb = Arc::clone(&input_cb);
Box::new(move |input: gpui::PlatformInput| -> DispatchEventResult { cb.lock()(input) })
};
{
let cb = Arc::clone(&input_cb);
let scale_factor = self.window.scale_factor();
let momentum = Arc::clone(&self.momentum);
const SCROLL_SLOP: f32 = 8.0;
#[derive(Clone, Copy, Debug)]
enum TouchState {
Idle,
Pending { start_x: f32, start_y: f32 },
Scrolling { prev_x: f32, prev_y: f32 },
}
let state = Mutex::new(TouchState::Idle);
self.window.on_touch(move |touch| {
let logical_x = touch.x / scale_factor;
let logical_y = touch.y / scale_factor;
let modifiers = gpui::Modifiers::default();
let mut ts = state.lock();
match touch.action {
0 => {
{
let mut ms = momentum.lock();
ms.scroller.cancel();
ms.velocity_tracker.reset();
ms.pending_scroll_dx = 0.0;
ms.pending_scroll_dy = 0.0;
ms.has_pending_scroll = false;
}
*ts = TouchState::Pending {
start_x: logical_x,
start_y: logical_y,
};
}
2 => {
let mut ms = momentum.lock();
ms.velocity_tracker.record(logical_x, logical_y);
match *ts {
TouchState::Pending { start_x, start_y } => {
let dx = logical_x - start_x;
let dy = logical_y - start_y;
let distance = (dx * dx + dy * dy).sqrt();
if distance > SCROLL_SLOP {
*ts = TouchState::Scrolling {
prev_x: logical_x,
prev_y: logical_y,
};
ms.pending_scroll_dx += dx;
ms.pending_scroll_dy += dy;
ms.pending_scroll_pos_x = logical_x;
ms.pending_scroll_pos_y = logical_y;
if !ms.has_pending_scroll {
ms.pending_scroll_phase = gpui::TouchPhase::Started;
}
ms.has_pending_scroll = true;
}
}
TouchState::Scrolling { prev_x, prev_y } => {
let dx = logical_x - prev_x;
let dy = logical_y - prev_y;
*ts = TouchState::Scrolling {
prev_x: logical_x,
prev_y: logical_y,
};
ms.pending_scroll_dx += dx;
ms.pending_scroll_dy += dy;
ms.pending_scroll_pos_x = logical_x;
ms.pending_scroll_pos_y = logical_y;
if !ms.has_pending_scroll {
ms.pending_scroll_phase = gpui::TouchPhase::Moved;
}
ms.has_pending_scroll = true;
}
TouchState::Idle => {
}
}
drop(ms);
let position = gpui::point(gpui::px(logical_x), gpui::px(logical_y));
let mut guard = cb.lock();
let _ = guard(gpui::PlatformInput::MouseMove(gpui::MouseMoveEvent {
position,
modifiers,
pressed_button: Some(gpui::MouseButton::Left),
}));
}
1 | 3 => {
let position = gpui::point(gpui::px(logical_x), gpui::px(logical_y));
match *ts {
TouchState::Pending { start_x, start_y } => {
{
let mut ms = momentum.lock();
ms.velocity_tracker.reset();
ms.has_pending_scroll = false;
}
let tap_pos = gpui::point(gpui::px(start_x), gpui::px(start_y));
let mut guard = cb.lock();
let _ =
guard(gpui::PlatformInput::MouseDown(gpui::MouseDownEvent {
button: gpui::MouseButton::Left,
position: tap_pos,
modifiers,
click_count: 1,
first_mouse: false,
}));
let _ = guard(gpui::PlatformInput::MouseUp(gpui::MouseUpEvent {
button: gpui::MouseButton::Left,
position: tap_pos,
modifiers,
click_count: 1,
}));
}
TouchState::Scrolling { prev_x, prev_y } => {
let dx = logical_x - prev_x;
let dy = logical_y - prev_y;
let mut ms = momentum.lock();
let total_dx = ms.pending_scroll_dx + dx;
let total_dy = ms.pending_scroll_dy + dy;
ms.pending_scroll_dx = 0.0;
ms.pending_scroll_dy = 0.0;
ms.has_pending_scroll = false;
let (vx, vy) = ms.velocity_tracker.velocity();
ms.velocity_tracker.reset();
ms.scroller.fling(vx, vy, logical_x, logical_y);
drop(ms);
let mut guard = cb.lock();
let _ = guard(gpui::PlatformInput::ScrollWheel(
gpui::ScrollWheelEvent {
position,
delta: gpui::ScrollDelta::Pixels(gpui::point(
gpui::px(total_dx),
gpui::px(total_dy),
)),
modifiers,
touch_phase: gpui::TouchPhase::Ended,
},
));
let _ = guard(gpui::PlatformInput::MouseUp(gpui::MouseUpEvent {
button: gpui::MouseButton::Left,
position,
modifiers,
click_count: 1,
}));
}
TouchState::Idle => {}
}
*ts = TouchState::Idle;
}
_ => {} }
});
}
{
let cb = Arc::clone(&input_cb);
self.window.on_key_event(move |key_event| {
use crate::android::keyboard::{
android_key_to_keystroke, AKEY_EVENT_ACTION_DOWN, AKEY_EVENT_ACTION_UP,
};
let keystroke = match android_key_to_keystroke(
key_event.key_code,
key_event.meta_state,
key_event.unicode_char,
) {
Some(ks) => ks,
None => return, };
let event = if key_event.action == AKEY_EVENT_ACTION_DOWN {
gpui::PlatformInput::KeyDown(gpui::KeyDownEvent {
keystroke,
is_held: false,
prefer_character_input: key_event.unicode_char != 0,
})
} else if key_event.action == AKEY_EVENT_ACTION_UP {
gpui::PlatformInput::KeyUp(gpui::KeyUpEvent { keystroke })
} else {
return; };
let mut guard = cb.lock();
let _ = guard(event);
});
}
}
fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
let send_callback: Box<dyn FnMut(bool) + Send> = unsafe { std::mem::transmute(callback) };
let send_callback = Mutex::new(send_callback);
self.window.on_active_status_change(move |active| {
let mut cb = send_callback.lock();
cb(active);
});
}
fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>) {
let _callback = Mutex::new(callback);
}
fn on_resize(&self, callback: Box<dyn FnMut(gpui::Size<gpui::Pixels>, f32)>) {
let send_callback: Box<dyn FnMut(gpui::Size<gpui::Pixels>, f32) + Send> =
unsafe { std::mem::transmute(callback) };
let send_callback = Arc::new(Mutex::new(send_callback));
self.window.on_resize(move |device_size, scale| {
let mut cb = send_callback.lock();
cb(
gpui::size(
gpui::px(device_size.width.0 as f32 / scale),
gpui::px(device_size.height.0 as f32 / scale),
),
scale,
);
});
}
fn on_moved(&self, _callback: Box<dyn FnMut()>) {
}
fn on_should_close(&self, _callback: Box<dyn FnMut() -> bool>) {
}
fn on_hit_test_window_control(&self, _callback: Box<dyn FnMut() -> Option<WindowControlArea>>) {
}
fn on_close(&self, callback: Box<dyn FnOnce()>) {
let send_callback: Box<dyn FnOnce() + Send + 'static> =
unsafe { std::mem::transmute::<Box<dyn FnOnce()>, Box<dyn FnOnce() + Send>>(callback) };
self.window.on_close(send_callback);
}
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
let send_callback: Box<dyn FnMut() + Send> = unsafe { std::mem::transmute(callback) };
let send_callback = Mutex::new(send_callback);
self.window.on_appearance_changed(move |_appearance| {
let mut cb = send_callback.lock();
cb();
});
}
fn draw(&self, scene: &gpui::Scene) {
log::trace!(
"AndroidPlatformWindow::draw — {} quads, {} shadows",
scene.quads.len(),
scene.shadows.len(),
);
self.window.draw(scene);
}
fn completed_frame(&self) {
}
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
self.window
.sprite_atlas()
.unwrap_or_else(|| Arc::new(FallbackAtlas::new()))
}
fn is_subpixel_rendering_supported(&self) -> bool {
self.window.supports_subpixel_aa()
}
fn gpu_specs(&self) -> Option<GpuSpecs> {
self.window.gpu_specs()
}
fn update_ime_position(&self, bounds: gpui::Bounds<gpui::Pixels>) {
use crate::android::jni_entry;
use std::ffi::c_void;
let vm = jni_entry::java_vm();
if vm.is_null() {
return;
}
let activity_obj = jni_entry::activity_as_ptr();
if activity_obj.is_null() {
return;
}
unsafe {
let env = jni_entry::jni_fns::get_env_from_vm(vm);
if env.is_null() {
return;
}
let fn_table = *(env as *const *const *const c_void);
macro_rules! jni_fn {
($idx:expr, $ty:ty) => {
std::mem::transmute::<*const c_void, $ty>(*fn_table.add($idx))
};
}
type FindClassFn = unsafe extern "C" fn(*mut c_void, *const i8) -> *mut c_void;
type GetMethodIDFn =
unsafe extern "C" fn(*mut c_void, *mut c_void, *const i8, *const i8) -> *mut c_void;
type CallObjectMethodAFn = unsafe extern "C" fn(
*mut c_void,
*mut c_void,
*mut c_void,
*const i64,
) -> *mut c_void;
type CallVoidMethodAFn =
unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, *const i64);
type NewStringUtfFn = unsafe extern "C" fn(*mut c_void, *const i8) -> *mut c_void;
type NewObjectAFn = unsafe extern "C" fn(
*mut c_void,
*mut c_void,
*mut c_void,
*const i64,
) -> *mut c_void;
type CallIntMethodAFn =
unsafe extern "C" fn(*mut c_void, *mut c_void, *mut c_void, *const i64) -> i32;
type DeleteLocalRefFn = unsafe extern "C" fn(*mut c_void, *mut c_void);
type ExceptionCheckFn = unsafe extern "C" fn(*mut c_void) -> u8;
type ExceptionClearFn = unsafe extern "C" fn(*mut c_void);
let find_class: FindClassFn = jni_fn!(6, FindClassFn);
let get_method_id: GetMethodIDFn = jni_fn!(33, GetMethodIDFn);
let call_object_method_a: CallObjectMethodAFn = jni_fn!(36, CallObjectMethodAFn);
let call_void_method_a: CallVoidMethodAFn = jni_fn!(61, CallVoidMethodAFn);
let new_string_utf: NewStringUtfFn = jni_fn!(167, NewStringUtfFn);
let new_object_a: NewObjectAFn = jni_fn!(30, NewObjectAFn);
let delete_local_ref: DeleteLocalRefFn = jni_fn!(23, DeleteLocalRefFn);
let exception_check: ExceptionCheckFn = jni_fn!(228, ExceptionCheckFn);
let exception_clear: ExceptionClearFn = jni_fn!(17, ExceptionClearFn);
let context_cls = find_class(env, b"android/content/Context\0".as_ptr() as *const i8);
if context_cls.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
return;
}
let get_system_service = get_method_id(
env,
context_cls,
b"getSystemService\0".as_ptr() as *const i8,
b"(Ljava/lang/String;)Ljava/lang/Object;\0".as_ptr() as *const i8,
);
if get_system_service.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, context_cls);
return;
}
let service_name = new_string_utf(env, b"input_method\0".as_ptr() as *const i8);
if service_name.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, context_cls);
return;
}
let args: [i64; 1] = [service_name as i64];
let imm = call_object_method_a(
env,
activity_obj as *mut c_void,
get_system_service,
args.as_ptr(),
);
delete_local_ref(env, service_name);
delete_local_ref(env, context_cls);
if imm.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
return;
}
let builder_cls = find_class(
env,
b"android/view/inputmethod/CursorAnchorInfo$Builder\0".as_ptr() as *const i8,
);
if builder_cls.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, imm);
return;
}
let builder_init = get_method_id(
env,
builder_cls,
b"<init>\0".as_ptr() as *const i8,
b"()V\0".as_ptr() as *const i8,
);
if builder_init.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, builder_cls);
delete_local_ref(env, imm);
return;
}
let no_args: [i64; 0] = [];
let builder = new_object_a(env, builder_cls, builder_init, no_args.as_ptr());
if builder.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, builder_cls);
delete_local_ref(env, imm);
return;
}
let set_marker = get_method_id(
env,
builder_cls,
b"setInsertionMarkerLocation\0".as_ptr() as *const i8,
b"(FFFFI)Landroid/view/inputmethod/CursorAnchorInfo$Builder;\0".as_ptr()
as *const i8,
);
if !set_marker.is_null() {
let x: f32 = bounds.origin.x.into();
let y: f32 = bounds.origin.y.into();
let h: f32 = bounds.size.height.into();
let marker_args: [i64; 5] = [
f32::to_bits(x) as i64, f32::to_bits(y) as i64, f32::to_bits(y + h * 0.8) as i64, f32::to_bits(y + h) as i64, 0i64, ];
let _ = call_object_method_a(env, builder, set_marker, marker_args.as_ptr());
if exception_check(env) != 0 {
exception_clear(env);
}
}
let build_method = get_method_id(
env,
builder_cls,
b"build\0".as_ptr() as *const i8,
b"()Landroid/view/inputmethod/CursorAnchorInfo;\0".as_ptr() as *const i8,
);
if build_method.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, builder);
delete_local_ref(env, builder_cls);
delete_local_ref(env, imm);
return;
}
let anchor_info = call_object_method_a(env, builder, build_method, no_args.as_ptr());
delete_local_ref(env, builder);
delete_local_ref(env, builder_cls);
if anchor_info.is_null() {
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, imm);
return;
}
let activity_cls = find_class(env, b"android/app/Activity\0".as_ptr() as *const i8);
if !activity_cls.is_null() {
let get_window = get_method_id(
env,
activity_cls,
b"getWindow\0".as_ptr() as *const i8,
b"()Landroid/view/Window;\0".as_ptr() as *const i8,
);
if !get_window.is_null() {
let app_window = call_object_method_a(
env,
activity_obj as *mut c_void,
get_window,
no_args.as_ptr(),
);
if !app_window.is_null() {
let window_cls =
find_class(env, b"android/view/Window\0".as_ptr() as *const i8);
if !window_cls.is_null() {
let get_decor = get_method_id(
env,
window_cls,
b"getDecorView\0".as_ptr() as *const i8,
b"()Landroid/view/View;\0".as_ptr() as *const i8,
);
if !get_decor.is_null() {
let decor_view = call_object_method_a(
env,
app_window,
get_decor,
no_args.as_ptr(),
);
if !decor_view.is_null() {
let imm_cls = find_class(
env,
b"android/view/inputmethod/InputMethodManager\0".as_ptr()
as *const i8,
);
if !imm_cls.is_null() {
let update_method = get_method_id(
env,
imm_cls,
b"updateCursorAnchorInfo\0".as_ptr() as *const i8,
b"(Landroid/view/View;Landroid/view/inputmethod/CursorAnchorInfo;)V\0"
.as_ptr() as *const i8,
);
if !update_method.is_null() {
let update_args: [i64; 2] =
[decor_view as i64, anchor_info as i64];
call_void_method_a(
env,
imm,
update_method,
update_args.as_ptr(),
);
if exception_check(env) != 0 {
exception_clear(env);
}
log::trace!(
"update_ime_position: x={:.0} y={:.0} h={:.0}",
f32::from(bounds.origin.x),
f32::from(bounds.origin.y),
f32::from(bounds.size.height)
);
}
delete_local_ref(env, imm_cls);
}
delete_local_ref(env, decor_view);
}
}
delete_local_ref(env, window_cls);
}
delete_local_ref(env, app_window);
}
}
delete_local_ref(env, activity_cls);
}
if exception_check(env) != 0 {
exception_clear(env);
}
delete_local_ref(env, anchor_info);
delete_local_ref(env, imm);
}
}
}
struct FallbackAtlas {
state: Mutex<FallbackAtlasState>,
}
struct FallbackAtlasState {
next_id: u32,
tiles: HashMap<AtlasKey, AtlasTile>,
}
impl FallbackAtlas {
fn new() -> Self {
Self {
state: Mutex::new(FallbackAtlasState {
next_id: 1,
tiles: HashMap::new(),
}),
}
}
}
impl PlatformAtlas for FallbackAtlas {
fn get_or_insert_with<'a>(
&self,
key: &AtlasKey,
build: &mut dyn FnMut() -> anyhow::Result<
Option<(gpui::Size<gpui::DevicePixels>, std::borrow::Cow<'a, [u8]>)>,
>,
) -> anyhow::Result<Option<AtlasTile>> {
let mut state = self.state.lock();
if let Some(tile) = state.tiles.get(key) {
return Ok(Some(tile.clone()));
}
let data = build()?;
if let Some((size, _pixels)) = data {
let id = state.next_id;
state.next_id += 1;
let tile = AtlasTile {
texture_id: gpui::AtlasTextureId {
index: 0,
kind: gpui::AtlasTextureKind::Monochrome,
},
tile_id: gpui::TileId(id),
padding: 0,
bounds: gpui::Bounds {
origin: gpui::point(gpui::DevicePixels(0), gpui::DevicePixels(0)),
size,
},
};
state.tiles.insert(key.clone(), tile.clone());
Ok(Some(tile))
} else {
Ok(None)
}
}
fn remove(&self, key: &AtlasKey) {
self.state.lock().tiles.remove(key);
}
}
#[derive(Default)]
pub struct WindowList {
windows: Vec<Arc<AndroidWindow>>,
}
impl WindowList {
pub fn push(&mut self, window: Arc<AndroidWindow>) {
self.windows.push(window);
}
pub fn remove(&mut self, id: u64) -> Option<Arc<AndroidWindow>> {
if let Some(pos) = self.windows.iter().position(|w| w.id() == id) {
Some(self.windows.remove(pos))
} else {
None
}
}
pub fn get(&self, id: u64) -> Option<&Arc<AndroidWindow>> {
self.windows.iter().find(|w| w.id() == id)
}
pub fn primary(&self) -> Option<&Arc<AndroidWindow>> {
self.windows.first()
}
pub fn iter(&self) -> impl Iterator<Item = &Arc<AndroidWindow>> {
self.windows.iter()
}
pub fn len(&self) -> usize {
self.windows.len()
}
pub fn is_empty(&self) -> bool {
self.windows.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn headless_window_geometry() {
let w = AndroidWindow::headless(1080, 1920, 3.0);
assert_eq!(w.physical_size().width, DevicePixels(1080));
assert_eq!(w.physical_size().height, DevicePixels(1920));
assert!((w.scale_factor() - 3.0).abs() < f32::EPSILON);
assert!(!w.has_surface());
assert!(!w.is_active());
}
#[test]
fn headless_window_logical_size() {
let w = AndroidWindow::headless(1080, 1920, 3.0);
let ls = w.logical_size();
assert!((ls.width.0 - 360.0).abs() < f32::EPSILON);
assert!((ls.height.0 - 640.0).abs() < f32::EPSILON);
}
#[test]
fn headless_window_bounds_origin_is_zero() {
let w = AndroidWindow::headless(1080, 1920, 2.0);
let b = w.bounds();
assert_eq!(b.origin.x, DevicePixels(0));
assert_eq!(b.origin.y, DevicePixels(0));
}
#[test]
fn window_id_is_stable() {
let w = AndroidWindow::headless(1080, 1920, 2.0);
assert_eq!(w.id(), w.id());
}
#[test]
fn window_appearance_defaults_to_light() {
let w = AndroidWindow::headless(1080, 1920, 2.0);
assert_eq!(w.appearance(), WindowAppearance::Light);
}
#[test]
fn window_set_appearance_dark() {
let w = AndroidWindow::headless(1080, 1920, 2.0);
w.set_appearance(WindowAppearance::Dark);
assert_eq!(w.appearance(), WindowAppearance::Dark);
}
#[test]
fn window_appearance_callback_fires() {
let w = AndroidWindow::headless(1080, 1920, 2.0);
let fired = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let fired_clone = fired.clone();
w.on_appearance_changed(move |_| {
fired_clone.store(true, std::sync::atomic::Ordering::Relaxed);
});
w.set_appearance(WindowAppearance::Dark);
assert!(fired.load(std::sync::atomic::Ordering::Relaxed));
}
#[test]
fn window_appearance_callback_not_fired_if_unchanged() {
let w = AndroidWindow::headless(1080, 1920, 2.0);
let fired = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let fired_clone = fired.clone();
w.on_appearance_changed(move |_| {
fired_clone.store(true, std::sync::atomic::Ordering::Relaxed);
});
w.set_appearance(WindowAppearance::Light);
assert!(!fired.load(std::sync::atomic::Ordering::Relaxed));
}
#[test]
fn touch_callback_fires() {
let w = AndroidWindow::headless(1080, 1920, 2.0);
let received = std::sync::Arc::new(parking_lot::Mutex::new(Vec::<TouchPoint>::new()));
let r2 = received.clone();
w.on_touch(move |pt| {
r2.lock().push(pt);
});
w.handle_touch(TouchPoint {
id: 1,
x: 100.0,
y: 200.0,
action: 0,
});
let pts = received.lock();
assert_eq!(pts.len(), 1);
assert_eq!(pts[0].id, 1);
}
#[test]
fn key_callback_fires() {
let w = AndroidWindow::headless(1080, 1920, 2.0);
let received = std::sync::Arc::new(parking_lot::Mutex::new(Vec::<AndroidKeyEvent>::new()));
let r2 = received.clone();
w.on_key_event(move |e| {
r2.lock().push(e);
});
w.handle_key_event(AndroidKeyEvent {
key_code: 29, action: 0,
meta_state: 0,
unicode_char: b'a' as u32,
});
let events = received.lock();
assert_eq!(events.len(), 1);
assert_eq!(events[0].key_code, 29);
}
#[test]
fn request_frame_callback_fires() {
let w = AndroidWindow::headless(1080, 1920, 2.0);
let count = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
let c2 = count.clone();
w.on_request_frame(move || {
c2.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
});
w.request_frame();
w.request_frame();
assert_eq!(count.load(std::sync::atomic::Ordering::Relaxed), 2);
}
#[test]
fn window_list_push_get_remove() {
let mut list = WindowList::default();
let w = AndroidWindow::headless(1080, 1920, 2.0);
let id = w.id();
list.push(w);
assert_eq!(list.len(), 1);
assert!(list.get(id).is_some());
let removed = list.remove(id);
assert!(removed.is_some());
assert!(list.is_empty());
assert!(list.get(id).is_none());
}
#[test]
fn window_list_primary() {
let mut list = WindowList::default();
list.push(AndroidWindow::headless(1080, 1920, 2.0));
assert!(list.primary().is_some());
}
#[test]
fn window_list_remove_missing_returns_none() {
let mut list = WindowList::default();
assert!(list.remove(0xDEADBEEF).is_none());
}
#[test]
fn subpixel_aa_false_for_headless() {
let w = AndroidWindow::headless(1080, 1920, 2.0);
assert!(!w.supports_subpixel_aa());
}
#[test]
fn gpu_info_none_for_headless() {
let w = AndroidWindow::headless(1080, 1920, 2.0);
assert!(w.gpu_info().is_none());
}
}