Skip to content
This repository was archived by the owner on Jul 27, 2023. It is now read-only.

Commit d430630

Browse files
authored
Merge pull request #51 from xi-editor/anim_frame
Add basic support for animations
2 parents 7ac464c + fb48a6c commit d430630

File tree

3 files changed

+206
-4
lines changed

3 files changed

+206
-4
lines changed

xi-win-ui/examples/anim.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Copyright 2018 The xi-editor Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
//! Example of animation frames.
16+
17+
extern crate xi_win_shell;
18+
extern crate xi_win_ui;
19+
extern crate direct2d;
20+
extern crate directwrite;
21+
22+
use direct2d::brush::SolidColorBrush;
23+
use direct2d::RenderTarget;
24+
25+
use xi_win_shell::win_main;
26+
use xi_win_shell::window::WindowBuilder;
27+
28+
use xi_win_ui::{UiMain, UiState, UiInner};
29+
30+
use xi_win_ui::{BoxConstraints, Geometry, LayoutResult};
31+
use xi_win_ui::{HandlerCtx, Id, LayoutCtx, MouseEvent, PaintCtx};
32+
use xi_win_ui::widget::Widget;
33+
34+
/// A custom widget with animations.
35+
struct AnimWidget(f32);
36+
37+
impl Widget for AnimWidget {
38+
fn paint(&mut self, paint_ctx: &mut PaintCtx, geom: &Geometry) {
39+
let rt = paint_ctx.render_target();
40+
let fg = SolidColorBrush::create(rt).with_color(0xf0f0ea).build().unwrap();
41+
let (x, y) = geom.pos;
42+
rt.draw_line((x, y), (x + geom.size.0, y + self.0 * geom.size.1),
43+
&fg, 1.0, None);
44+
}
45+
46+
fn layout(&mut self, bc: &BoxConstraints, _children: &[Id], _size: Option<(f32, f32)>,
47+
_ctx: &mut LayoutCtx) -> LayoutResult
48+
{
49+
LayoutResult::Size(bc.constrain((100.0, 100.0)))
50+
}
51+
52+
fn anim_frame(&mut self, interval: u64, ctx: &mut HandlerCtx) {
53+
println!("anim frame, interval={}", interval);
54+
if self.0 > 0.0 {
55+
ctx.request_anim_frame();
56+
self.0 = (self.0 - 1e-9 * (interval as f32)).max(0.0);
57+
}
58+
ctx.invalidate();
59+
}
60+
61+
fn mouse(&mut self, event: &MouseEvent, ctx: &mut HandlerCtx) -> bool {
62+
if event.count > 0 {
63+
self.0 = 1.0;
64+
ctx.request_anim_frame();
65+
}
66+
true
67+
}
68+
}
69+
70+
impl AnimWidget {
71+
fn ui(self, ctx: &mut UiInner) -> Id {
72+
ctx.add(self, &[])
73+
}
74+
}
75+
76+
fn main() {
77+
xi_win_shell::init();
78+
79+
let mut run_loop = win_main::RunLoop::new();
80+
let mut builder = WindowBuilder::new();
81+
let mut state = UiState::new();
82+
let anim = AnimWidget(1.0).ui(&mut state);
83+
state.set_root(anim);
84+
builder.set_handler(Box::new(UiMain::new(state)));
85+
builder.set_title("Animation example");
86+
let window = builder.build().unwrap();
87+
window.show();
88+
run_loop.run();
89+
}

xi-win-ui/src/lib.rs

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ use std::char;
2525
use std::collections::BTreeMap;
2626
use std::mem;
2727
use std::ops::{Deref, DerefMut};
28+
use std::time::Instant;
2829

2930
use direct2d::math::*;
3031
use direct2d::RenderTarget;
@@ -81,6 +82,18 @@ pub struct LayoutCtx {
8182
/// Bounding box of each widget. The position is relative to the parent.
8283
geom: Vec<Geometry>,
8384

85+
/// Additional state per widget.
86+
///
87+
/// A case can be made to fold `geom` here instead of having a separate array;
88+
/// this is the general SOA vs AOS discussion.
89+
per_widget: Vec<PerWidgetState>,
90+
91+
/// State of animation request.
92+
anim_state: AnimState,
93+
94+
/// The time of the last paint cycle.
95+
prev_paint_time: Option<Instant>,
96+
8497
/// Queue of events to distribute to listeners
8598
event_q: Vec<(Id, Box<Any>)>,
8699

@@ -108,6 +121,18 @@ pub struct Geometry {
108121
pub size: (f32, f32),
109122
}
110123

124+
#[derive(Default)]
125+
struct PerWidgetState {
126+
anim_frame_requested: bool,
127+
}
128+
129+
enum AnimState {
130+
Idle,
131+
InvalidationRequested,
132+
AnimFrameStart,
133+
AnimFrameRequested,
134+
}
135+
111136
#[derive(Clone, Copy)]
112137
pub struct BoxConstraints {
113138
min_width: f32,
@@ -188,6 +213,9 @@ impl UiState {
188213
c: LayoutCtx {
189214
dwrite_factory: directwrite::Factory::new().unwrap(),
190215
geom: Vec::new(),
216+
per_widget: Vec::new(),
217+
anim_state: AnimState::Idle,
218+
prev_paint_time: None,
191219
handle: Default::default(),
192220
event_q: Vec::new(),
193221
focused: None,
@@ -385,6 +413,35 @@ impl UiState {
385413
}
386414
}
387415

416+
// Process an animation frame. This consists mostly of calling anim_frame on
417+
// widgets that have requested a frame.
418+
fn anim_frame(&mut self) {
419+
// TODO: this is just wall-clock time, which will have jitter making
420+
// animations not as smooth. Should be extracting actual refresh rate
421+
// from presentation statistics and then doing some processing.
422+
let this_paint_time = Instant::now();
423+
let interval = if let Some(last) = self.c.prev_paint_time {
424+
let duration = this_paint_time.duration_since(last);
425+
1_000_000_000 * duration.as_secs() + (duration.subsec_nanos() as u64)
426+
} else {
427+
0
428+
};
429+
self.c.anim_state = AnimState::AnimFrameStart;
430+
for node in 0..self.widgets.len() {
431+
if self.c.per_widget[node].anim_frame_requested {
432+
self.c.per_widget[node].anim_frame_requested = false;
433+
self.inner.widgets[node].anim_frame(interval,
434+
&mut HandlerCtx {
435+
id: node,
436+
c: &mut self.inner.c,
437+
}
438+
);
439+
}
440+
}
441+
self.c.prev_paint_time = Some(this_paint_time);
442+
self.dispatch_events();
443+
}
444+
388445
/// Translate coordinates to local coordinates of widget
389446
fn xy_to_local(&mut self, mut node: Id, mut x: f32, mut y: f32) -> (f32, f32) {
390447
loop {
@@ -444,6 +501,7 @@ impl UiInner {
444501
let id = self.graph.alloc_node();
445502
self.widgets.push(Box::new(widget));
446503
self.c.geom.push(Default::default());
504+
self.c.per_widget.push(Default::default());
447505
for &child in children {
448506
self.graph.append_child(id, child);
449507
}
@@ -545,8 +603,14 @@ impl LayoutCtx {
545603
impl<'a> HandlerCtx<'a> {
546604
/// Invalidate this widget. Finer-grained invalidation is not yet implemented,
547605
/// but when it is, this method will invalidate the widget's bounding box.
548-
pub fn invalidate(&self) {
549-
self.c.handle.invalidate();
606+
pub fn invalidate(&mut self) {
607+
match self.c.anim_state {
608+
AnimState::Idle => {
609+
self.c.handle.invalidate();
610+
self.c.anim_state = AnimState::InvalidationRequested;
611+
}
612+
_ => (),
613+
}
550614
}
551615

552616
/// Send an event, to be handled by listeners.
@@ -570,6 +634,23 @@ impl<'a> HandlerCtx<'a> {
570634
pub fn is_hot(&self) -> bool {
571635
self.c.hot == Some(self.id) && (self.is_active() || self.c.active.is_none())
572636
}
637+
638+
/// Request an animation frame.
639+
///
640+
/// Calling this schedules an animation frame, and also causes `anim_frame` to be
641+
/// called on this widget at the beginning of that frame.
642+
pub fn request_anim_frame(&mut self) {
643+
self.c.per_widget[self.id].anim_frame_requested = true;
644+
match self.c.anim_state {
645+
AnimState::Idle => {
646+
self.invalidate();
647+
}
648+
AnimState::AnimFrameStart => {
649+
self.c.anim_state = AnimState::AnimFrameRequested;
650+
}
651+
_ => (),
652+
}
653+
}
573654
}
574655

575656
impl<'a> Deref for ListenerCtx<'a> {
@@ -640,6 +721,8 @@ impl WinHandler for UiMain {
640721
}
641722

642723
fn paint(&self, paint_ctx: &mut paint::PaintCtx) -> bool {
724+
let mut state = self.state.borrow_mut();
725+
state.anim_frame();
643726
let size;
644727
{
645728
let rt = paint_ctx.render_target();
@@ -648,13 +731,19 @@ impl WinHandler for UiMain {
648731
let bg = SolidColorBrush::create(rt).with_color(0x272822).build().unwrap();
649732
rt.fill_rectangle(rect, &bg);
650733
}
651-
let mut state = self.state.borrow_mut();
652734
let root = state.graph.root;
653735
let bc = BoxConstraints::tight((size.width, size.height));
654736
// TODO: be lazier about relayout
655737
state.layout(&bc, root);
656738
state.paint(paint_ctx, root);
657-
false
739+
match state.c.anim_state {
740+
AnimState::AnimFrameRequested => true,
741+
_ => {
742+
state.c.anim_state = AnimState::Idle;
743+
state.c.prev_paint_time = None;
744+
false
745+
}
746+
}
658747
}
659748

660749
fn command(&self, id: u32) {

xi-win-ui/src/widget/mod.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,30 @@ pub trait Widget {
9898
/// Returns true if the event is handled.
9999
#[allow(unused)]
100100
fn key(&mut self, event: &KeyEvent, ctx: &mut HandlerCtx) -> bool { false }
101+
102+
/// Called at the beginning of a new animation frame.
103+
///
104+
/// The `interval` argument is the time in nanoseconds between frames, for
105+
/// the purpose of computing animations. When framerate is steady, it should
106+
/// be exactly the reciprocal of the refresh rate of the monitor. If we are
107+
/// skipping frames, its cumulative sum should approximately track the
108+
/// passage of wall clock time and otherwise should be chosen to optimize
109+
/// for smoothness of animations.
110+
///
111+
/// The ideal heuristic for computing this interval is a deep topic, ideally
112+
/// the subject of a blog post when I figure it out.
113+
///
114+
/// On the first frame when transitioning from idle to animating, `interval`
115+
/// will be 0. (This logic is presently per-window but might change to
116+
/// per-widget to make it more consistent).
117+
///
118+
/// This method should call `invalidate` on the context if the visual display
119+
/// updates. In the current implementation, that's not very useful, but it
120+
/// will become much more so when there's fine grained invalidation.
121+
///
122+
/// The method can also call `request_anim_frame` to keep the animation running.
123+
#[allow(unused)]
124+
fn anim_frame(&mut self, interval: u64, ctx: &mut HandlerCtx) {}
101125
}
102126

103127
pub struct MouseEvent {

0 commit comments

Comments
 (0)