use std::cell::RefCell;
use crate::animation::Tween;
use crate::geometry::Rect;
use crate::widget::Widget;
pub(crate) const SAFETY_MARGIN: f64 = 8.0;
const LIFT_DURATION_SECS: f64 = 0.22;
thread_local! {
static LIFT: RefCell<Tween> = RefCell::new(Tween::new(0.0, LIFT_DURATION_SECS));
}
pub fn request_lift(target: f64) {
LIFT.with(|c| c.borrow_mut().set_target(target.max(0.0)));
crate::animation::request_draw();
}
pub fn current_lift() -> f64 {
LIFT.with(|c| c.borrow().value())
}
pub fn tick_lift() -> f64 {
LIFT.with(|c| c.borrow_mut().tick())
}
pub fn is_lift_animating() -> bool {
LIFT.with(|c| c.borrow().is_animating())
}
#[cfg(test)]
pub fn reset_lift_for_test() {
LIFT.with(|c| *c.borrow_mut() = Tween::new(0.0, LIFT_DURATION_SECS));
}
#[cfg(test)]
pub fn lift_target_for_test() -> f64 {
LIFT.with(|c| c.borrow().target())
}
#[inline]
pub fn lift_to_world(screen_pos: crate::geometry::Point) -> crate::geometry::Point {
let lift = current_lift();
if lift.abs() < 0.001 {
screen_pos
} else {
crate::geometry::Point::new(screen_pos.x, screen_pos.y - lift)
}
}
pub(crate) fn paint_lifted_tree(
root: &mut dyn Widget,
ctx: &mut dyn crate::draw_ctx::DrawCtx,
viewport: crate::geometry::Size,
lift: f64,
) {
use crate::widget::paint::{paint_global_overlays, paint_subtree};
let lifted = lift.abs() > 0.001;
if lifted {
ctx.save();
ctx.translate(0.0, lift);
}
paint_subtree(root, ctx);
crate::widgets::combo_box::paint_global_combo_popups(ctx);
crate::widgets::tooltip::paint_global_tooltips(ctx, viewport);
paint_global_overlays(root, ctx);
crate::widgets::combo_box::paint_global_combo_popups(ctx);
crate::widgets::tooltip::paint_global_tooltips(ctx, viewport);
if lifted {
ctx.restore();
}
}
pub(crate) fn notify_focus_change(
new_path: Option<&[usize]>,
viewport_width: f64,
root: &mut dyn Widget,
) {
use crate::widgets::on_screen_keyboard::{set_text_input_focused, KeyboardInputMode};
let (accepts, existing_text, mode) = match new_path {
Some(p) => {
let w = mutable_widget_at_path(root, p);
(
w.accepts_text_input(),
w.text_input_value(),
w.text_input_mode(),
)
}
None => (false, None, KeyboardInputMode::Text),
};
set_text_input_focused(accepts, existing_text.as_deref(), mode);
if accepts {
ensure_focused_visible_above_keyboard(new_path, viewport_width, root);
} else {
request_lift(0.0);
}
}
pub(crate) fn ensure_focused_visible_above_keyboard(
focus: Option<&[usize]>,
viewport_width: f64,
root: &mut dyn Widget,
) {
let Some(path) = focus else {
return;
};
let Some(rect) = focused_widget_screen_bounds(&*root, path) else {
return;
};
let panel_h = crate::widgets::on_screen_keyboard::target_panel_height(viewport_width);
if panel_h <= 0.0 {
return;
}
let required = panel_h + SAFETY_MARGIN;
if rect.y >= required {
request_lift(0.0);
return;
}
let deficit = required - rect.y;
let absorbed_by_scroll = apply_lift_along_path(root, path, deficit);
let residual = (deficit - absorbed_by_scroll).max(0.0);
request_lift(residual);
}
pub(crate) fn focused_widget_screen_bounds(root: &dyn Widget, path: &[usize]) -> Option<Rect> {
let mut to_screen = crate::TransAffine::new();
let mut widget: &dyn Widget = root;
for &idx in path.iter() {
let b = widget.bounds();
to_screen.translate(b.x, b.y);
if let Some(ct) = widget.child_transform() {
to_screen.premultiply(&ct);
}
let children = widget.children();
if idx >= children.len() {
return None;
}
widget = children[idx].as_ref();
}
Some(transform_rect_aabb(&to_screen, widget.bounds()))
}
fn transform_rect_aabb(t: &crate::TransAffine, rect: Rect) -> Rect {
let corners = [
(rect.x, rect.y),
(rect.x + rect.width, rect.y),
(rect.x, rect.y + rect.height),
(rect.x + rect.width, rect.y + rect.height),
];
let (mut min_x, mut min_y) = (f64::INFINITY, f64::INFINITY);
let (mut max_x, mut max_y) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
for (mut x, mut y) in corners {
t.transform(&mut x, &mut y);
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
}
Rect::new(min_x, min_y, (max_x - min_x).max(0.0), (max_y - min_y).max(0.0))
}
pub(crate) fn apply_lift_along_path(
root: &mut dyn Widget,
path: &[usize],
mut deficit: f64,
) -> f64 {
if deficit.abs() < 0.5 {
return 0.0;
}
let mut total_applied = 0.0;
let n = path.len();
if n == 0 {
return 0.0;
}
for ancestor_depth in (0..n).rev() {
let ancestor_path = &path[..ancestor_depth];
let ancestor = mutable_widget_at_path(root, ancestor_path);
let applied = ancestor.try_scroll_to_lift(deficit);
total_applied += applied;
deficit -= applied;
if deficit.abs() < 0.5 {
break;
}
}
total_applied
}
fn mutable_widget_at_path<'a>(root: &'a mut dyn Widget, path: &[usize]) -> &'a mut dyn Widget {
if path.is_empty() {
return root;
}
let idx = path[0];
let child = &mut root.children_mut()[idx];
mutable_widget_at_path(child.as_mut(), &path[1..])
}
#[cfg(test)]
mod tests {
use super::focused_widget_screen_bounds;
use crate::draw_ctx::DrawCtx;
use crate::event::{Event, EventResult};
use crate::geometry::{Rect, Size};
use crate::widget::Widget;
use crate::TransAffine;
struct ScaleParent {
bounds: Rect,
children: Vec<Box<dyn Widget>>,
zoom: f64,
offset: (f64, f64),
}
impl Widget for ScaleParent {
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, b: Rect) {
self.bounds = b;
}
fn children(&self) -> &[Box<dyn Widget>] {
&self.children
}
fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
&mut self.children
}
fn layout(&mut self, a: Size) -> Size {
a
}
fn paint(&mut self, _: &mut dyn DrawCtx) {}
fn on_event(&mut self, _: &Event) -> EventResult {
EventResult::Ignored
}
fn child_transform(&self) -> Option<TransAffine> {
let mut m = TransAffine::new_scaling_uniform(self.zoom);
m.translate(self.offset.0, self.offset.1);
Some(m)
}
}
struct Leaf {
bounds: Rect,
children: Vec<Box<dyn Widget>>,
}
impl Widget for Leaf {
fn bounds(&self) -> Rect {
self.bounds
}
fn set_bounds(&mut self, b: Rect) {
self.bounds = b;
}
fn children(&self) -> &[Box<dyn Widget>] {
&self.children
}
fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
&mut self.children
}
fn layout(&mut self, _: Size) -> Size {
Size::new(self.bounds.width, self.bounds.height)
}
fn paint(&mut self, _: &mut dyn DrawCtx) {}
fn on_event(&mut self, _: &Event) -> EventResult {
EventResult::Ignored
}
}
#[test]
fn focused_bounds_honor_child_transform() {
let leaf = Leaf {
bounds: Rect::new(5.0, 5.0, 8.0, 4.0),
children: vec![],
};
let parent = ScaleParent {
bounds: Rect::new(0.0, 0.0, 400.0, 300.0),
children: vec![Box::new(leaf)],
zoom: 2.0,
offset: (10.0, 20.0),
};
let r = focused_widget_screen_bounds(&parent, &[0]).expect("bounds");
assert!(
(r.x - 20.0).abs() < 1e-6
&& (r.y - 30.0).abs() < 1e-6
&& (r.width - 16.0).abs() < 1e-6
&& (r.height - 8.0).abs() < 1e-6,
"focused bounds must reflect the parent's child_transform; got {:?}",
r
);
}
#[test]
fn focused_bounds_plain_bounds_accumulate() {
let leaf = Leaf {
bounds: Rect::new(5.0, 7.0, 8.0, 4.0),
children: vec![],
};
let parent = ScaleParent {
bounds: Rect::new(30.0, 40.0, 400.0, 300.0),
children: vec![Box::new(leaf)],
zoom: 1.0,
offset: (0.0, 0.0),
};
let r = focused_widget_screen_bounds(&parent, &[0]).expect("bounds");
assert!(
(r.x - 35.0).abs() < 1e-6
&& (r.y - 47.0).abs() < 1e-6
&& (r.width - 8.0).abs() < 1e-6
&& (r.height - 4.0).abs() < 1e-6,
"plain bounds accumulation regressed; got {:?}",
r
);
}
}