use std::sync::{
atomic::{AtomicBool, AtomicI32, Ordering},
Arc, Mutex,
};
static PENDING_IME_INSET_PX: AtomicI32 = AtomicI32::new(-1);
static PENDING_SAFE_AREA_TOP_PX: AtomicI32 = AtomicI32::new(-1);
static PENDING_SAFE_AREA_RIGHT_PX: AtomicI32 = AtomicI32::new(-1);
static PENDING_SAFE_AREA_BOTTOM_PX: AtomicI32 = AtomicI32::new(-1);
static PENDING_SAFE_AREA_LEFT_PX: AtomicI32 = AtomicI32::new(-1);
static PENDING_KEY_DOWN_EVENTS: Mutex<Vec<(u32, u32)>> = Mutex::new(Vec::new());
use android_activity::input::{
InputEvent as AndroidInputEvent, KeyAction, KeyMapChar, Keycode, MotionAction,
};
use android_activity::{AndroidApp as NdkAndroidApp, InputStatus, MainEvent, PollEvent};
use ndk::native_window::NativeWindow;
use blinc_animation::AnimationScheduler;
use blinc_core::context_state::{BlincContextState, HookState, SharedHookState};
use blinc_core::reactive::{ReactiveGraph, SignalId};
use blinc_layout::event_router::MouseButton;
use blinc_layout::overlay_state::OverlayContext;
use blinc_layout::prelude::*;
use blinc_layout::widgets::overlay::{overlay_manager, OverlayManager};
use blinc_platform::assets::set_global_asset_loader;
use blinc_platform_android::input::{detect_pinch, PinchPhase, PinchState, TouchPointer};
use blinc_platform_android::AndroidAssetLoader;
use crate::app::BlincApp;
use crate::error::{BlincError, Result};
use crate::windowed::{
RefDirtyFlag, SharedAnimationScheduler, SharedElementRegistry, SharedReactiveGraph,
SharedReadyCallbacks, WindowedContext,
};
pub struct AndroidApp;
impl AndroidApp {
fn init_asset_loader(app: NdkAndroidApp) {
let loader = AndroidAssetLoader::new(app);
let _ = set_global_asset_loader(Box::new(loader));
}
fn init_theme() {
use blinc_theme::{
detect_system_color_scheme, platform_theme_bundle, set_redraw_callback, ThemeState,
};
if ThemeState::try_get().is_none() {
let bundle = platform_theme_bundle();
let scheme = detect_system_color_scheme();
ThemeState::init(bundle, scheme);
}
set_redraw_callback(|| {
tracing::debug!("Theme changed - requesting full rebuild");
blinc_layout::widgets::request_full_rebuild();
});
}
fn init_logging() {
android_logger::init_once(
android_logger::Config::default()
.with_max_level(log::LevelFilter::Debug)
.with_tag("Blinc"),
);
use tracing_subscriber::layer::SubscriberExt;
let subscriber =
tracing_subscriber::registry().with(tracing_android::layer("Blinc").unwrap());
let _ = tracing::subscriber::set_global_default(subscriber);
}
pub fn run<F, E>(app: NdkAndroidApp, mut ui_builder: F) -> Result<()>
where
F: FnMut(&mut WindowedContext) -> E + 'static,
E: ElementBuilder + 'static,
{
Self::init_logging();
tracing::info!("AndroidApp::run starting");
Self::init_asset_loader(app.clone());
crate::text_measurer::init_text_measurer();
Self::init_theme();
let ref_dirty_flag: RefDirtyFlag = Arc::new(AtomicBool::new(false));
let reactive: SharedReactiveGraph = Arc::new(Mutex::new(ReactiveGraph::new()));
let hooks: SharedHookState = Arc::new(Mutex::new(HookState::new()));
if !BlincContextState::is_initialized() {
#[allow(clippy::type_complexity)]
let stateful_callback: Arc<dyn Fn(&[SignalId]) + Send + Sync> =
Arc::new(|signal_ids| {
blinc_layout::check_stateful_deps(signal_ids);
});
BlincContextState::init_with_callback(
Arc::clone(&reactive),
Arc::clone(&hooks),
Arc::clone(&ref_dirty_flag),
stateful_callback,
);
}
let scheduler = AnimationScheduler::new();
let animations: SharedAnimationScheduler = Arc::new(Mutex::new(scheduler));
{
let scheduler_handle = animations.lock().unwrap().handle();
blinc_animation::set_global_scheduler(scheduler_handle);
}
let element_registry: SharedElementRegistry =
Arc::new(blinc_layout::selector::ElementRegistry::new());
{
let registry_for_query = Arc::clone(&element_registry);
let query_callback: blinc_core::QueryCallback = Arc::new(move |id: &str| {
registry_for_query.get(id).map(|node_id| node_id.to_raw())
});
BlincContextState::get().set_query_callback(query_callback);
}
{
let registry_for_bounds = Arc::clone(&element_registry);
let bounds_callback: blinc_core::BoundsCallback =
Arc::new(move |id: &str| registry_for_bounds.get_bounds(id));
BlincContextState::get().set_bounds_callback(bounds_callback);
}
BlincContextState::get()
.set_element_registry(Arc::clone(&element_registry) as blinc_core::AnyElementRegistry);
let ready_callbacks: SharedReadyCallbacks = Arc::new(Mutex::new(Vec::new()));
let overlays: OverlayManager = overlay_manager();
if !OverlayContext::is_initialized() {
OverlayContext::init(Arc::clone(&overlays));
}
blinc_theme::ThemeState::get().set_scheduler(&animations);
let shared_motion_states = blinc_layout::create_shared_motion_states();
{
let motion_states_for_callback = Arc::clone(&shared_motion_states);
let motion_callback: blinc_core::MotionStateCallback = Arc::new(move |key: &str| {
motion_states_for_callback
.read()
.ok()
.and_then(|states| states.get(key).copied())
.unwrap_or(blinc_core::MotionAnimationState::NotFound)
});
BlincContextState::get().set_motion_state_callback(motion_callback);
}
let mut blinc_app: Option<BlincApp> = None;
let mut surface: Option<wgpu::Surface<'static>> = None;
let mut surface_config: Option<wgpu::SurfaceConfiguration> = None;
let mut ctx: Option<WindowedContext> = None;
let mut render_tree: Option<RenderTree> = None;
let mut render_state: Option<blinc_layout::RenderState> = None;
let mut native_window: Option<NativeWindow> = None;
let mut needs_rebuild = true;
let mut needs_redraw_next_frame = false;
let mut last_frame_time_ms: u64 = 0;
let mut running = true;
let mut focused = false;
let mut last_applied_keyboard_inset_px: i32 = -1;
let mut last_applied_safe_area_top_px: i32 = -1;
let mut last_applied_safe_area_right_px: i32 = -1;
let mut last_applied_safe_area_bottom_px: i32 = -1;
let mut last_applied_safe_area_left_px: i32 = -1;
let mut last_focus_tap_generation: u64 = 0;
let mut last_touch_x: Option<f32> = None;
let mut last_touch_y: Option<f32> = None;
let mut is_scrolling = false;
let mut pinch_state = PinchState::default();
tracing::info!("Entering Android event loop");
while running {
let long_press_pending = blinc_layout::widgets::text_input::is_long_press_armed();
let poll_timeout = if needs_rebuild || needs_redraw_next_frame {
Some(std::time::Duration::ZERO) } else if long_press_pending {
Some(std::time::Duration::from_millis(33))
} else {
Some(std::time::Duration::from_millis(100)) };
needs_redraw_next_frame = false;
app.poll_events(poll_timeout, |event| {
match event {
PollEvent::Main(main_event) => match main_event {
MainEvent::InitWindow { .. } => {
tracing::info!("Native window initialized");
if let Some(window) = app.native_window() {
let width = window.width() as u32;
let height = window.height() as u32;
tracing::info!("Window size: {}x{}", width, height);
match Self::init_gpu(&window) {
Ok((app_instance, surf)) => {
let format = app_instance.texture_format();
let alpha_mode = wgpu::CompositeAlphaMode::Inherit;
tracing::info!(
"Android surface: format={:?}, alpha_mode={:?}, size={}x{}",
format,
alpha_mode,
width,
height,
);
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format,
width,
height,
present_mode: wgpu::PresentMode::AutoVsync,
alpha_mode,
view_formats: vec![],
desired_maximum_frame_latency: 2,
};
surf.configure(app_instance.device(), &config);
crate::text_measurer::init_text_measurer_with_registry(
app_instance.font_registry(),
);
surface = Some(surf);
surface_config = Some(config);
blinc_app = Some(app_instance);
native_window = Some(window);
let scale_factor =
blinc_platform_android::get_display_density(&app);
let logical_width = width as f32 / scale_factor as f32;
let logical_height = height as f32 / scale_factor as f32;
ctx = Some(WindowedContext::new_android(
logical_width,
logical_height,
scale_factor,
width as f32,
height as f32,
focused,
(0.0, 0.0, 0.0, 0.0),
Arc::clone(&animations),
Arc::clone(&ref_dirty_flag),
Arc::clone(&reactive),
Arc::clone(&hooks),
Arc::clone(&overlays),
Arc::clone(&element_registry),
Arc::clone(&ready_callbacks),
));
BlincContextState::get()
.set_viewport_size(logical_width, logical_height);
let mut rs =
blinc_layout::RenderState::new(Arc::clone(&animations));
rs.set_shared_motion_states(Arc::clone(
&shared_motion_states,
));
render_state = Some(rs);
needs_rebuild = true;
tracing::info!("GPU initialized successfully");
}
Err(e) => {
tracing::error!("Failed to initialize GPU: {}", e);
}
}
}
}
MainEvent::TerminateWindow { .. } => {
tracing::info!("Native window terminated");
native_window = None;
surface = None;
surface_config = None;
blinc_app = None;
ctx = None;
render_tree = None;
render_state = None;
}
MainEvent::WindowResized { .. } => {
if let Some(ref window) = native_window {
let width = window.width() as u32;
let height = window.height() as u32;
tracing::info!("Window resized: {}x{}", width, height);
if let (
Some(ref app_instance),
Some(ref surf),
Some(ref mut config),
) = (&blinc_app, &surface, &mut surface_config)
{
if width > 0 && height > 0 {
config.width = width;
config.height = height;
surf.configure(app_instance.device(), config);
if let Some(ref mut windowed_ctx) = ctx {
let scale_factor = windowed_ctx.scale_factor;
windowed_ctx.width = width as f32 / scale_factor as f32;
windowed_ctx.height =
height as f32 / scale_factor as f32;
BlincContextState::get().set_viewport_size(
windowed_ctx.width,
windowed_ctx.height,
);
}
needs_rebuild = true;
}
}
}
}
MainEvent::GainedFocus => {
tracing::info!("App gained focus");
focused = true;
if let Some(ref mut windowed_ctx) = ctx {
windowed_ctx.focused = true;
}
}
MainEvent::LostFocus => {
tracing::info!("App lost focus");
focused = false;
if let Some(ref mut windowed_ctx) = ctx {
windowed_ctx.focused = false;
}
}
MainEvent::Resume { .. } => {
tracing::info!("App resumed");
focused = true;
}
MainEvent::Pause => {
tracing::info!("App paused");
focused = false;
}
MainEvent::Destroy => {
tracing::info!("App destroyed");
running = false;
}
MainEvent::LowMemory => {
tracing::warn!("Low memory warning");
}
_ => {}
},
PollEvent::Wake => {
needs_redraw_next_frame = true;
}
_ => {}
}
});
#[derive(Clone, Default)]
struct PendingEvent {
node_id: blinc_layout::LayoutNodeId,
event_type: u32,
mouse_x: f32,
mouse_y: f32,
}
let mut pending_events: Vec<PendingEvent> = Vec::new();
let mut scroll_info: Option<(f32, f32, f32, f32)> = None;
let mut touch_ended = false;
if let (Some(ref mut windowed_ctx), Some(ref mut tree)) = (&mut ctx, &mut render_tree) {
let scale = windowed_ctx.scale_factor as f32;
let router = &mut windowed_ctx.event_router;
router.set_event_callback({
let events = &mut pending_events as *mut Vec<PendingEvent>;
move |node, event_type| {
unsafe {
(*events).push(PendingEvent {
node_id: node,
event_type,
..Default::default()
});
}
}
});
match app.input_events_iter() {
Ok(mut input_iter) => {
while input_iter.next(|event| {
match event {
AndroidInputEvent::MotionEvent(motion_event) => {
let action = motion_event.action();
let pointer_count = motion_event.pointer_count();
let action_index = motion_event.pointer_index();
if pointer_count == 0 {
if action == MotionAction::Cancel {
tracing::debug!("Touch CANCEL");
windowed_ctx.pointer_query.set_pressure(0.0);
windowed_ctx.pointer_query.set_touch_count(0);
router.on_mouse_leave();
pinch_state.reset();
last_touch_x = None;
last_touch_y = None;
if is_scrolling {
touch_ended = true;
}
is_scrolling = false;
return InputStatus::Handled;
}
return InputStatus::Unhandled;
}
if pointer_count > 0 {
let pointer_idx = match action {
MotionAction::PointerDown | MotionAction::PointerUp => {
action_index
}
_ => 0,
};
let pointer = motion_event.pointer_at_index(pointer_idx);
let lx = pointer.x() / scale;
let ly = pointer.y() / scale;
let pointers: Vec<TouchPointer> =
(0..pointer_count)
.map(|i| {
let p = motion_event.pointer_at_index(i);
TouchPointer {
id: p.pointer_id(),
x: p.x() / scale,
y: p.y() / scale,
pressure: p.pressure(),
size: p.size(),
}
})
.collect();
if let Some(primary) = pointers.first() {
windowed_ctx.pointer_query.set_pressure(primary.pressure);
}
windowed_ctx.pointer_query.set_touch_count(pointers.len() as u32);
let pinch_gesture = detect_pinch(&pointers, &mut pinch_state);
if let Some(gesture) = pinch_gesture {
if matches!(gesture.phase, PinchPhase::Started | PinchPhase::Moved)
{
if let Some(hit) =
router.hit_test(tree, gesture.center.0, gesture.center.1)
{
tree.dispatch_pinch_chain(
&hit,
gesture.center.0,
gesture.center.1,
gesture.scale,
);
needs_redraw_next_frame = true;
}
}
if matches!(gesture.phase, PinchPhase::Started | PinchPhase::Ended)
{
last_touch_x = None;
last_touch_y = None;
is_scrolling = false;
}
}
match action {
MotionAction::Down | MotionAction::PointerDown => {
tracing::debug!(
"Touch DOWN at logical ({:.1}, {:.1})",
lx,
ly
);
blinc_layout::widgets::text_input::set_touch_input(true);
blinc_layout::widgets::blur_all_text_inputs();
router.on_mouse_down(
&*tree,
lx,
ly,
MouseButton::Left,
);
if pointer_count == 1 {
last_touch_x = Some(lx);
last_touch_y = Some(ly);
is_scrolling = false;
}
unsafe {
let events = &mut pending_events
as *mut Vec<PendingEvent>;
for event in (*events).iter_mut() {
event.mouse_x = lx;
event.mouse_y = ly;
}
}
}
MotionAction::Move => {
router.on_mouse_move(&*tree, lx, ly);
if pointer_count == 1 {
if let (Some(prev_x), Some(prev_y)) =
(last_touch_x, last_touch_y)
{
let delta_x = lx - prev_x;
let delta_y = ly - prev_y;
if delta_x.abs() > 0.5 || delta_y.abs() > 0.5 {
is_scrolling = true;
scroll_info =
Some((lx, ly, delta_x, delta_y));
tracing::trace!(
"Touch scroll: delta=({:.1}, {:.1})",
delta_x,
delta_y
);
}
}
last_touch_x = Some(lx);
last_touch_y = Some(ly);
}
unsafe {
let events = &mut pending_events
as *mut Vec<PendingEvent>;
for event in (*events).iter_mut() {
event.mouse_x = lx;
event.mouse_y = ly;
}
}
}
MotionAction::Up | MotionAction::PointerUp => {
tracing::debug!(
"Touch UP at logical ({:.1}, {:.1})",
lx,
ly
);
blinc_layout::widgets::text_input::cancel_long_press_timer();
windowed_ctx.pointer_query.set_pressure(0.0);
router.on_mouse_up(&*tree, lx, ly, MouseButton::Left);
if is_scrolling {
touch_ended = true;
}
last_touch_x = None;
last_touch_y = None;
is_scrolling = false;
unsafe {
let events = &mut pending_events
as *mut Vec<PendingEvent>;
for event in (*events).iter_mut() {
event.mouse_x = lx;
event.mouse_y = ly;
}
}
}
MotionAction::Cancel => {
tracing::debug!("Touch CANCEL");
blinc_layout::widgets::text_input::cancel_long_press_timer();
windowed_ctx.pointer_query.set_pressure(0.0);
windowed_ctx.pointer_query.set_touch_count(0);
router.on_mouse_leave();
pinch_state.reset();
last_touch_x = None;
last_touch_y = None;
if is_scrolling {
touch_ended = true;
}
is_scrolling = false;
}
_ => {}
}
InputStatus::Handled
} else {
InputStatus::Unhandled
}
}
AndroidInputEvent::KeyEvent(key_event) => {
if key_event.action() == KeyAction::Down {
let key_code = key_event.key_code();
let meta_state = key_event.meta_state();
let virtual_key = match key_code {
Keycode::Del => Some(8u32),
Keycode::Enter | Keycode::NumpadEnter => Some(13u32),
Keycode::Escape => Some(27u32),
Keycode::DpadLeft => Some(37u32),
Keycode::DpadUp => Some(38u32),
Keycode::DpadRight => Some(39u32),
Keycode::DpadDown => Some(40u32),
Keycode::MoveHome => Some(36u32),
Keycode::MoveEnd => Some(35u32),
_ => None,
};
if let Some(vkey) = virtual_key {
tree.broadcast_key_event(
blinc_core::events::event_types::KEY_DOWN,
vkey,
false,
false,
false,
false,
);
} else {
if let Ok(map) = app.device_key_character_map(
key_event.device_id(),
) {
if let Ok(KeyMapChar::Unicode(ch)) =
map.get(key_code, meta_state)
{
tree.broadcast_text_input_event(
ch, false, false, false, false,
);
}
}
}
}
InputStatus::Handled
}
_ => InputStatus::Unhandled,
}
}) {
}
}
Err(e) => {
tracing::warn!("Failed to get input events iterator: {:?}", e);
}
}
router.clear_event_callback();
} else {
if ctx.is_none() {
tracing::trace!("Input: ctx is None");
}
if render_tree.is_none() {
tracing::trace!("Input: render_tree is None");
}
}
if !pending_events.is_empty() {
if let (Some(ref mut tree), Some(ref windowed_ctx)) = (&mut render_tree, &ctx) {
let router = &windowed_ctx.event_router;
for event in pending_events {
let (bounds_x, bounds_y, bounds_width, bounds_height) = router
.get_node_bounds(event.node_id)
.unwrap_or((0.0, 0.0, 0.0, 0.0));
let local_x = event.mouse_x - bounds_x;
let local_y = event.mouse_y - bounds_y;
tracing::debug!(
"Dispatching event: node={:?}, type={}, pos=({:.1}, {:.1}), local=({:.1}, {:.1})",
event.node_id,
event.event_type,
event.mouse_x,
event.mouse_y,
local_x,
local_y,
);
tree.dispatch_event_full(
event.node_id,
event.event_type,
event.mouse_x,
event.mouse_y,
local_x,
local_y,
bounds_x,
bounds_y,
bounds_width,
bounds_height,
0.0, 0.0, 1.0, );
}
}
}
if let Some((mouse_x, mouse_y, delta_x, delta_y)) = scroll_info {
if let (Some(ref mut windowed_ctx), Some(ref mut tree)) =
(&mut ctx, &mut render_tree)
{
let router = &mut windowed_ctx.event_router;
if let Some(hit) = router.hit_test(tree, mouse_x, mouse_y) {
let scroll_time = blinc_layout::prelude::elapsed_ms() as f64;
tracing::debug!(
"Dispatching scroll: hit={:?}, delta=({:.1}, {:.1})",
hit.node,
delta_x,
delta_y
);
tree.dispatch_scroll_chain_with_time(
hit.node,
&hit.ancestors,
mouse_x,
mouse_y,
delta_x,
delta_y,
scroll_time,
);
needs_redraw_next_frame = true;
}
}
}
if touch_ended {
if let Some(ref mut tree) = render_tree {
tracing::debug!("Touch ended - notifying scroll physics");
tree.on_scroll_end();
needs_redraw_next_frame = true;
}
}
let scroll_animating = if let Some(ref mut tree) = render_tree {
let current_time = blinc_layout::prelude::elapsed_ms();
let animating = tree.tick_scroll_physics(current_time);
tree.process_pending_scroll_refs();
animating
} else {
false
};
if scroll_animating {
needs_redraw_next_frame = true;
}
if let Some(show) = blinc_layout::widgets::text_input::take_keyboard_state_change() {
let bridge_ready = blinc_core::native_bridge::NativeBridgeState::is_initialized();
let routed_via_bridge = if bridge_ready {
let result: blinc_core::native_bridge::NativeResult<()> =
blinc_core::native_bridge::native_call(
"keyboard",
if show { "show" } else { "hide" },
(),
);
match result {
Ok(()) => true,
Err(e) => {
tracing::warn!(
"BlincNativeBridge keyboard.{} failed: {:?} — falling back to NDK helper",
if show { "show" } else { "hide" },
e
);
false
}
}
} else {
false
};
if !routed_via_bridge {
if show {
app.show_soft_input(true);
} else {
app.hide_soft_input(true);
}
}
}
let pending_sa_top = PENDING_SAFE_AREA_TOP_PX.load(Ordering::Relaxed);
let pending_sa_right = PENDING_SAFE_AREA_RIGHT_PX.load(Ordering::Relaxed);
let pending_sa_bottom = PENDING_SAFE_AREA_BOTTOM_PX.load(Ordering::Relaxed);
let pending_sa_left = PENDING_SAFE_AREA_LEFT_PX.load(Ordering::Relaxed);
let safe_area_ready = pending_sa_top >= 0
&& pending_sa_right >= 0
&& pending_sa_bottom >= 0
&& pending_sa_left >= 0;
let safe_area_changed = safe_area_ready
&& (pending_sa_top != last_applied_safe_area_top_px
|| pending_sa_right != last_applied_safe_area_right_px
|| pending_sa_bottom != last_applied_safe_area_bottom_px
|| pending_sa_left != last_applied_safe_area_left_px);
if safe_area_changed {
if let Some(ref mut windowed_ctx) = ctx {
windowed_ctx.safe_area = (
pending_sa_top as f32,
pending_sa_right as f32,
pending_sa_bottom as f32,
pending_sa_left as f32,
);
needs_redraw_next_frame = true;
tracing::debug!(
"Android safe area updated: top={} right={} bottom={} left={}",
pending_sa_top,
pending_sa_right,
pending_sa_bottom,
pending_sa_left,
);
}
last_applied_safe_area_top_px = pending_sa_top;
last_applied_safe_area_right_px = pending_sa_right;
last_applied_safe_area_bottom_px = pending_sa_bottom;
last_applied_safe_area_left_px = pending_sa_left;
}
let pending_inset_px = PENDING_IME_INSET_PX.load(Ordering::Relaxed);
let current_tap_gen = blinc_layout::widgets::text_input::focus_tap_generation();
let inset_changed =
pending_inset_px >= 0 && pending_inset_px != last_applied_keyboard_inset_px;
let tap_changed = current_tap_gen != last_focus_tap_generation;
let needs_scroll_pass = inset_changed || (tap_changed && pending_inset_px > 0);
if needs_scroll_pass {
let inset_to_apply = pending_inset_px.max(0) as f32;
if let Some(ref mut windowed_ctx) = ctx {
windowed_ctx.keyboard_inset = inset_to_apply;
let viewport_h = windowed_ctx.height;
if let Some(ref mut tree) = render_tree {
let scrolled = tree
.scroll_focused_text_input_above_keyboard(viewport_h, inset_to_apply);
if scrolled {
needs_redraw_next_frame = true;
}
}
tracing::debug!(
"Android keyboard inset: last={} pending={} viewport_h={} tap_gen={}->{} inset_changed={} tap_changed={}",
last_applied_keyboard_inset_px,
pending_inset_px,
windowed_ctx.height,
last_focus_tap_generation,
current_tap_gen,
inset_changed,
tap_changed,
);
}
if pending_inset_px >= 0 {
last_applied_keyboard_inset_px = pending_inset_px;
}
last_focus_tap_generation = current_tap_gen;
}
let key_events_to_dispatch: Vec<(u32, u32)> = {
if let Ok(mut queue) = PENDING_KEY_DOWN_EVENTS.lock() {
std::mem::take(&mut *queue)
} else {
Vec::new()
}
};
if !key_events_to_dispatch.is_empty() {
if let Some(ref mut tree) = render_tree {
for (key_code, modifiers) in key_events_to_dispatch {
let shift = modifiers & 0x01 != 0;
let ctrl = modifiers & 0x02 != 0;
let alt = modifiers & 0x04 != 0;
let meta = modifiers & 0x08 != 0;
tracing::debug!(
"Dispatching synthesized KEY_DOWN: key_code={}, mods={:#x}",
key_code,
modifiers
);
tree.broadcast_key_event(
blinc_core::events::event_types::KEY_DOWN,
key_code,
shift,
ctrl,
alt,
meta,
);
}
needs_redraw_next_frame = true;
}
}
if blinc_layout::widgets::text_input::fire_long_press_timer_if_due() {
needs_redraw_next_frame = true;
}
let mut needs_redraw = false;
let has_stateful_updates = blinc_layout::take_needs_redraw();
let has_pending_rebuilds = blinc_layout::has_pending_subtree_rebuilds();
if has_stateful_updates || has_pending_rebuilds {
if has_stateful_updates {
tracing::debug!("Redraw requested by: stateful state change");
}
let prop_updates = blinc_layout::take_pending_prop_updates();
let had_prop_updates = !prop_updates.is_empty();
if let Some(ref mut tree) = render_tree {
for (node_id, props) in &prop_updates {
tree.update_render_props(*node_id, |p| *p = props.clone());
}
}
let mut needs_layout = false;
if let Some(ref mut tree) = render_tree {
needs_layout = tree.process_pending_subtree_rebuilds();
}
if needs_layout {
if let Some(ref mut tree) = render_tree {
if let Some(ref windowed_ctx) = ctx {
tracing::debug!("Subtree rebuilds processed, recomputing layout");
tree.compute_layout(windowed_ctx.width, windowed_ctx.height);
tree.apply_flip_transitions();
tree.update_flip_bounds();
}
}
}
if had_prop_updates && !needs_layout {
tracing::trace!("Visual-only prop updates, skipping layout");
}
needs_redraw = true;
}
if ref_dirty_flag.swap(false, Ordering::SeqCst) {
tracing::debug!("Rebuild triggered by: ref_dirty_flag (State::set)");
needs_rebuild = true;
}
if let Some(ref tree) = render_tree {
if tree.needs_rebuild() {
tracing::debug!("Rebuild triggered by: tree.needs_rebuild()");
needs_rebuild = true;
}
}
let animations_active = {
if let Ok(mut sched) = animations.lock() {
sched.tick()
} else {
false
}
};
if animations_active {
needs_redraw = true;
needs_redraw_next_frame = true;
}
static REBUILD_COUNT: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
if needs_rebuild && focused {
let count = REBUILD_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if count % 60 == 0 {
tracing::warn!("REBUILD #{} (every 60th logged)", count);
}
if let (
Some(ref mut app_instance),
Some(ref surf),
Some(ref config),
Some(ref mut windowed_ctx),
Some(ref rs),
) = (
&mut blinc_app,
&surface,
&surface_config,
&mut ctx,
&render_state,
) {
let element = ui_builder(windowed_ctx);
blinc_layout::clear_stateful_base_updaters();
blinc_layout::click_outside::clear_click_outside_handlers();
if render_tree.is_none() {
let mut tree = RenderTree::from_element(&element);
tree.set_scale_factor(windowed_ctx.scale_factor as f32);
if let Some(ref stylesheet) = windowed_ctx.stylesheet {
tree.set_stylesheet_arc(stylesheet.clone());
}
tree.apply_all_stylesheet_styles();
tree.compute_layout(windowed_ctx.width, windowed_ctx.height);
tree.update_flip_bounds();
tree.start_all_css_animations();
tree.clear_dirty(); render_tree = Some(tree);
} else if let Some(ref mut tree) = render_tree {
*tree = RenderTree::from_element(&element);
tree.set_scale_factor(windowed_ctx.scale_factor as f32);
if let Some(ref stylesheet) = windowed_ctx.stylesheet {
tree.set_stylesheet_arc(stylesheet.clone());
}
tree.apply_all_stylesheet_styles();
tree.compute_layout(windowed_ctx.width, windowed_ctx.height);
tree.update_flip_bounds();
tree.start_all_css_animations();
tree.clear_dirty();
}
needs_redraw = true;
}
needs_rebuild = false;
}
{
let current_time = blinc_layout::prelude::elapsed_ms();
if let Some(ref mut tree) = render_tree {
let dt_ms = if last_frame_time_ms > 0 {
(current_time - last_frame_time_ms) as f32
} else {
16.0
};
{
let store = tree.css_anim_store();
let mut s = store.lock().unwrap();
s.tick(dt_ms);
}
let flip_active = tree.tick_flip_animations(dt_ms);
let css_active =
tree.css_has_active() || !tree.css_transitions_empty() || flip_active;
if let Some(ref windowed_ctx) = ctx {
if tree.stylesheet().is_some() {
tree.apply_stylesheet_state_styles(&windowed_ctx.event_router);
}
}
if css_active
|| !tree.css_transitions_empty()
|| tree.has_active_flip_animations()
{
tree.apply_all_css_animation_props();
tree.apply_all_css_transition_props();
tree.apply_flip_animation_props();
needs_redraw = true;
needs_redraw_next_frame = true;
if tree.apply_animated_layout_props() {
if let Some(ref windowed_ctx) = ctx {
tree.compute_layout(windowed_ctx.width, windowed_ctx.height);
tree.update_flip_bounds();
}
}
}
}
last_frame_time_ms = current_time;
}
static REDRAW_COUNT: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(0);
if needs_redraw && focused {
let count = REDRAW_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if count % 120 == 0 {
tracing::info!("REDRAW #{} (every 120th logged)", count);
}
if let (
Some(ref mut app_instance),
Some(ref surf),
Some(ref config),
Some(ref mut windowed_ctx),
Some(ref rs),
Some(ref tree),
) = (
&mut blinc_app,
&surface,
&surface_config,
&mut ctx,
&render_state,
&render_tree,
) {
let frame = surf.get_current_texture();
static SUBOPTIMAL_LOGGED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
match frame {
Ok(output) => {
if output.suboptimal
&& !SUBOPTIMAL_LOGGED
.swap(true, std::sync::atomic::Ordering::Relaxed)
{
tracing::warn!(
"SurfaceTexture is suboptimal — will reconfigure swapchain"
);
}
let view = output.texture.create_view(&Default::default());
if let Err(e) = app_instance.render_tree_with_motion(
tree,
rs,
&view,
config.width,
config.height,
) {
tracing::error!("Render error: {}", e);
}
let was_suboptimal = output.suboptimal;
output.present();
if was_suboptimal {
surf.configure(app_instance.device(), config);
}
}
Err(wgpu::SurfaceError::Lost) | Err(wgpu::SurfaceError::Outdated) => {
tracing::warn!("Surface lost / outdated — reconfiguring swapchain");
surf.configure(app_instance.device(), config);
}
Err(wgpu::SurfaceError::OutOfMemory) => {
tracing::error!("Out of GPU memory");
running = false;
}
Err(e) => {
tracing::error!("Surface error: {:?}", e);
}
}
windowed_ctx.rebuild_count += 1;
if windowed_ctx.rebuild_count == 1 {
if let Ok(mut callbacks) = ready_callbacks.lock() {
for callback in callbacks.drain(..) {
callback();
}
}
}
}
needs_rebuild = false;
}
{
if let Ok(scheduler) = animations.lock() {
if scheduler.has_active_animations() {
needs_redraw_next_frame = true;
}
}
if blinc_layout::has_animating_statefuls() {
needs_redraw_next_frame = true;
}
if blinc_layout::has_pending_subtree_rebuilds() {
needs_redraw_next_frame = true;
}
}
}
tracing::info!("AndroidApp::run exiting");
Ok(())
}
fn init_gpu(window: &NativeWindow) -> Result<(BlincApp, wgpu::Surface<'static>)> {
use blinc_gpu::{GpuRenderer, RendererConfig, TextRenderingContext};
if let Err(e) = window.set_buffers_geometry(
0,
0,
Some(ndk::hardware_buffer_format::HardwareBufferFormat::R8G8B8X8_UNORM),
) {
tracing::warn!(
"ANativeWindow_setBuffersGeometry(R8G8B8X8_UNORM) failed: {} \
— surface may composite with alpha and appear blank on PowerVR-class GPUs",
e
);
} else {
tracing::info!("ANativeWindow buffer format forced to R8G8B8X8_UNORM (opaque)");
}
let config = crate::BlincConfig::default();
let renderer_config = RendererConfig {
max_primitives: config.max_primitives,
max_glass_primitives: config.max_glass_primitives,
max_glyphs: config.max_glyphs,
sample_count: 1,
texture_format: None,
unified_text_rendering: true,
..RendererConfig::default()
};
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::VULKAN,
..Default::default()
});
use raw_window_handle::{
AndroidDisplayHandle, AndroidNdkWindowHandle, RawDisplayHandle, RawWindowHandle,
};
use std::ptr::NonNull;
let raw_window = NonNull::new(window.ptr().as_ptr() as *mut std::ffi::c_void)
.ok_or_else(|| BlincError::GpuInit("Invalid native window pointer".to_string()))?;
let window_handle = AndroidNdkWindowHandle::new(raw_window);
let display_handle = AndroidDisplayHandle::new();
let surface_target = wgpu::SurfaceTargetUnsafe::RawHandle {
raw_display_handle: RawDisplayHandle::Android(display_handle),
raw_window_handle: RawWindowHandle::AndroidNdk(window_handle),
};
let surface = unsafe {
instance
.create_surface_unsafe(surface_target)
.map_err(|e| BlincError::GpuInit(e.to_string()))?
};
let renderer = pollster::block_on(async {
GpuRenderer::with_instance_and_surface(instance, &surface, renderer_config).await
})
.map_err(|e| BlincError::GpuInit(e.to_string()))?;
let device = renderer.device_arc();
let queue = renderer.queue_arc();
let mut text_ctx = TextRenderingContext::new(device.clone(), queue.clone());
let mut fonts_loaded = 0;
for font_path in crate::system_font_paths() {
let path = std::path::Path::new(font_path);
tracing::debug!("Checking font path: {}", font_path);
if path.exists() {
match std::fs::read(path) {
Ok(data) => {
tracing::info!("Loading font from: {} ({} bytes)", font_path, data.len());
match text_ctx.load_font_data(data) {
Ok(_) => {
tracing::info!("Successfully loaded font: {}", font_path);
fonts_loaded += 1;
}
Err(e) => {
tracing::warn!("Failed to load font {}: {:?}", font_path, e);
}
}
}
Err(e) => {
tracing::warn!("Failed to read font file {}: {}", font_path, e);
}
}
} else {
tracing::debug!("Font path does not exist: {}", font_path);
}
}
tracing::info!("Loaded {} system fonts", fonts_loaded);
text_ctx.preload_fonts(&["Roboto", "Noto Sans", "Droid Sans"]);
text_ctx.preload_generic_styles(blinc_gpu::GenericFont::SansSerif, &[400, 700], false);
tracing::info!("Font preloading complete");
let ctx = crate::context::RenderContext::new(
renderer,
text_ctx,
device,
queue,
config.sample_count,
);
let app = BlincApp::from_context(ctx, config);
Ok((app, surface))
}
}
pub fn dispatch_deep_link(uri: &str) {
tracing::info!("Android deep link received: {}", uri);
blinc_router::dispatch_deep_link(uri);
}
pub fn dispatch_stream_data(stream_id: u64, data: &[u8]) {
blinc_core::native_bridge::dispatch_stream_data(
stream_id,
blinc_core::native_bridge::NativeValue::Bytes(data.to_vec()),
);
}
#[cfg(target_os = "android")]
#[no_mangle]
pub extern "system" fn Java_com_blinc_BlincNativeBridge_nativeDispatchKeyboardInset(
_env: jni::JNIEnv,
_class: jni::objects::JClass,
inset_logical_px: jni::sys::jint,
) {
let clamped = inset_logical_px.max(0);
PENDING_IME_INSET_PX.store(clamped, Ordering::Relaxed);
}
#[cfg(target_os = "android")]
#[no_mangle]
pub extern "system" fn Java_com_blinc_BlincNativeBridge_nativeDispatchSafeArea(
_env: jni::JNIEnv,
_class: jni::objects::JClass,
top_logical_px: jni::sys::jint,
right_logical_px: jni::sys::jint,
bottom_logical_px: jni::sys::jint,
left_logical_px: jni::sys::jint,
) {
PENDING_SAFE_AREA_TOP_PX.store(top_logical_px.max(0), Ordering::Relaxed);
PENDING_SAFE_AREA_RIGHT_PX.store(right_logical_px.max(0), Ordering::Relaxed);
PENDING_SAFE_AREA_BOTTOM_PX.store(bottom_logical_px.max(0), Ordering::Relaxed);
PENDING_SAFE_AREA_LEFT_PX.store(left_logical_px.max(0), Ordering::Relaxed);
}
#[cfg(target_os = "android")]
#[no_mangle]
pub extern "system" fn Java_com_blinc_BlincNativeBridge_nativeDispatchKeyDownWithModifiers(
_env: jni::JNIEnv,
_class: jni::objects::JClass,
key_code: jni::sys::jint,
modifiers: jni::sys::jint,
) {
if let Ok(mut queue) = PENDING_KEY_DOWN_EVENTS.lock() {
queue.push((key_code.max(0) as u32, modifiers.max(0) as u32));
}
}