playdate_ui_crank_indicator/
lib.rs

1#![cfg_attr(not(test), no_std)]
2
3#[macro_use]
4extern crate alloc;
5extern crate sys;
6
7use core::ffi::c_uint;
8use core::marker::PhantomData;
9
10use display::DisplayScale;
11use gfx::BitmapFlip;
12use gfx::BitmapFlipExt;
13use gfx::bitmap;
14use gfx::bitmap::Bitmap;
15use gfx::bitmap::table::BitmapTable;
16use sprite::Sprite;
17use sprite::SpriteType;
18use sys::ffi::PDRect;
19use sys::traits::AsRaw;
20
21use sprite::AnySprite;
22use sprite::prelude::*;
23use sprite::callback::update::SpriteUpdate;
24use sprite::callback::update;
25use sprite::callback::draw::SpriteDraw;
26use sprite::callback::draw;
27
28
29const CRANK_FRAME_COUNT: u8 = 12;
30const TEXT_FRAME_COUNT: u8 = 14;
31
32type MySprite = Sprite<State, sprite::api::Default>;
33type UpdHandle = update::Handle<true, MySprite, UpdateDraw>;
34type DrwHandle = draw::l2::Handle<true, MySprite, UpdHandle, UpdateDraw>;
35
36
37pub struct CrankIndicator {
38	sprite: DrwHandle,
39}
40
41impl CrankIndicator {
42	pub fn new(scale: DisplayScale) -> Result<Self, gfx::error::ApiError> {
43		let state = State::new(scale)?;
44
45		let sprite = Sprite::<_, sprite::api::Default>::new().into_update_handler::<UpdateDraw>();
46		sprite.set_ignores_draw_offset(true);
47		sprite.set_bounds(state.bounds());
48
49		sprite.set_userdata(state);
50		Ok(Self { sprite: sprite.into_draw_handler::<UpdateDraw>() })
51	}
52
53
54	pub fn set_scale(&self, scale: DisplayScale) { self.sprite.userdata().map(|state| state.set_scale(scale)); }
55
56	pub fn set_offset(&self, x: i8, y: i8) {
57		self.sprite
58		    .userdata()
59		    .map(|state| state.set_offset(Point::new(x, y)));
60	}
61}
62
63
64impl AsRaw for CrankIndicator {
65	type Type = sys::ffi::LCDSprite;
66	unsafe fn as_raw(&self) -> *mut Self::Type { self.sprite.as_raw() }
67}
68impl SpriteApi for CrankIndicator {
69	type Api = sprite::api::Default;
70
71	fn api(&self) -> Self::Api
72		where Self::Api: Copy {
73		self.sprite.api()
74	}
75
76	fn api_ref(&self) -> &Self::Api { self.sprite.api_ref() }
77}
78impl AnySprite for CrankIndicator {}
79
80impl SpriteType for CrankIndicator {
81	type Api = <Self as SpriteApi>::Api;
82	type Userdata = <UpdateDraw as SpriteType>::Userdata;
83}
84
85
86pub struct UpdateDraw<T: AnySprite = SpriteRef>(PhantomData<T>);
87
88impl<T: AnySprite> SpriteType for UpdateDraw<T> {
89	type Api = <T as SpriteApi>::Api;
90	type Userdata = State;
91	const FREE_ON_DROP: bool = false;
92}
93
94impl<T: AnySprite> SpriteUpdate for UpdateDraw<T> {
95	#[inline(always)]
96	fn on_update(s: &update::Handle<false, SharedSprite<Self::Userdata, Self::Api>, Self>) {
97		if let Some(state) = s.userdata() {
98			if state.update() {
99				s.set_bounds(state.bounds());
100				s.mark_dirty();
101			} else {
102				// skip draw, not dirty
103			}
104		}
105	}
106}
107
108impl<T: AnySprite> SpriteDraw for UpdateDraw<T> {
109	#[inline(always)]
110	fn on_draw(s: &draw::Handle<false, SharedSprite<Self::Userdata, Self::Api>, Self>, bounds: PDRect, _: PDRect) {
111		if let Some(state) = s.userdata() {
112			let gfx = state.gfx;
113			gfx.draw(&state.bubble, bounds.x as _, bounds.y as _, state.bubble_flip);
114
115			const NORM: BitmapFlip = BitmapFlip::Unflipped;
116
117			if let Some(crank) = state.crank_current.as_ref() {
118				gfx.draw(&crank, state.crank_pos.x as _, state.crank_pos.y as _, NORM);
119			} else if let Some(text) = state.text.as_ref() {
120				gfx.draw(
121				         &text,
122				         state.text_position.x as _,
123				         state.text_position.y as _,
124				         NORM,
125				);
126			}
127		}
128	}
129}
130
131
132pub struct State {
133	// background
134	bubble: Bitmap<gfx::api::Default>,
135	bubble_pos: Point<i16>,
136	bubble_size: Size<u8>,
137	bubble_flip: BitmapFlip,
138
139	/// Crank animation frames
140	crank: BitmapTable<gfx::api::Default>,
141	/// Position of current frame of the crank for render
142	crank_pos: Point<i16>,
143	/// Current frame of the crank animation
144	crank_current: Option<Bitmap>,
145
146	// frames of sequence
147	frame: u8,
148	frame_count: u8,
149
150	// text
151	text: Option<Bitmap<gfx::api::Default>>,
152	text_frame_count: u8,
153	text_offset: i16,
154	text_position: Point<i16>,
155
156	/// User set option clockwise
157	clockwise: bool,
158	/// User set option offset
159	offset: Point<i8>,
160	/// User set option scale
161	scale: DisplayScale,
162
163	/// Last draw moment
164	last_time: c_uint,
165	/// Need to reload bitmaps
166	dirty: bool,
167
168	// cached endpoints
169	system: system::System<system::api::Cache>,
170	display: display::Display<display::api::Cache>,
171	gfx: gfx::Graphics<gfx::api::Cache>,
172}
173
174impl State {
175	fn new(scale: DisplayScale) -> Result<Self, gfx::error::ApiError> {
176		let bubble = load_bubble_for_scale(scale)?;
177		let crank = load_crank_for_scale(scale)?;
178
179		let bubble_size = bubble.size();
180		let bubble_size = Size::new(bubble_size.0 as _, bubble_size.1 as _);
181
182		let mut this = Self { bubble,
183		                      bubble_pos: Point::new(0, 0),
184		                      bubble_size,
185		                      bubble_flip: BitmapFlip::Unflipped,
186		                      crank,
187		                      crank_current: None,
188		                      crank_pos: Point::new(0, 0),
189		                      frame: 1,
190		                      frame_count: CRANK_FRAME_COUNT * 3,
191		                      text: None,
192		                      text_frame_count: 0,
193		                      text_position: Point::new(0, 0),
194		                      text_offset: 0,
195		                      offset: Point::new(0, 0),
196		                      clockwise: true,
197		                      scale,
198		                      last_time: 0,
199		                      dirty: false,
200		                      system: system::System::new(),
201		                      display: display::Display::new(),
202		                      gfx: gfx::Graphics::new() };
203
204		this.load_text_if_needed()?;
205		this.calc_positions();
206
207		Ok(this)
208	}
209
210
211	fn calc_positions(&mut self) {
212		let crank_indicator_y = 210 / self.scale.as_u8();
213
214		if self.system.flipped() {
215			let y = self.display.height() as i16 - (crank_indicator_y - self.bubble_size.h / 2) as i16;
216			self.bubble_pos = Point::new(0, y);
217			self.bubble_flip = BitmapFlip::FlippedXY;
218			self.text_offset = 100 / self.scale.as_u8() as i16;
219		} else {
220			self.bubble_pos.x = self.display.width() as i16 - self.bubble_size.w as i16;
221			self.bubble_pos.y = crank_indicator_y as i16 - self.bubble_size.h as i16 / 2;
222			self.bubble_flip = BitmapFlip::Unflipped;
223			self.text_offset = 76 / self.scale.as_u8() as i16;
224		}
225
226		self.frame = 1;
227		self.frame_count = CRANK_FRAME_COUNT;
228
229		if let Some(text_frame_image) = &self.text {
230			self.text_frame_count = TEXT_FRAME_COUNT;
231			self.frame_count = CRANK_FRAME_COUNT + TEXT_FRAME_COUNT;
232
233			let x_offset = self.offset_correction_x();
234
235			let (tw, th) = text_frame_image.size();
236			let x = self.bubble_pos.x + x_offset + (self.text_offset - tw as i16) / 2;
237			let y = self.bubble_pos.y + self.offset.y as i16 + (self.bubble_size.h as i16 - th as i16) / 2;
238			self.text_position.x = x;
239			self.text_position.y = y;
240		} else {
241			self.text_frame_count = 0;
242			self.frame_count = CRANK_FRAME_COUNT;
243		}
244	}
245
246
247	fn load_text_if_needed(&mut self) -> Result<(), gfx::error::ApiError> {
248		if matches!(self.scale, DisplayScale::Normal | DisplayScale::Double) {
249			self.text = load_text_for_scale(self.scale)?.into();
250		} else {
251			self.text.take();
252		}
253		Ok(())
254	}
255
256
257	fn reload_bitmaps(&mut self) -> Result<(), gfx::error::ApiError> {
258		let bubble = load_bubble_for_scale(self.scale)?;
259		self.crank = load_crank_for_scale(self.scale)?;
260
261		let bubble_size = bubble.size();
262		self.bubble_size = Size::new(bubble_size.0 as _, bubble_size.1 as _);
263
264		self.bubble = bubble;
265
266		self.load_text_if_needed()?;
267
268		self.calc_positions();
269		self.dirty = false;
270
271		Ok(())
272	}
273
274	fn offset_correction_x(&self) -> i16 {
275		// if matches!(self.scale, DisplayScale::Double) {
276		// 	self.offset.x - 1
277		// } else {
278		// 	self.offset.x
279		// }
280
281		// this is better:
282		self.offset.x as i16
283	}
284
285	fn offset_correction_y(&self) -> i16 {
286		if matches!(self.scale, DisplayScale::Double | DisplayScale::Quad) {
287			self.offset.y as i16 + 1
288		} else {
289			self.offset.y as i16
290		}
291	}
292
293
294	fn set_scale(&mut self, scale: DisplayScale) {
295		self.scale = scale;
296		self.dirty = true;
297	}
298
299	fn set_offset(&mut self, offset: Point<i8>) {
300		self.offset = offset;
301		self.calc_positions();
302	}
303
304	fn update(&mut self) -> bool {
305		let mut dirty = self.dirty;
306		let last_frame = self.frame;
307		let crank_drawn = self.crank_current.is_some();
308
309
310		if self.dirty {
311			self.reload_bitmaps().ok();
312		}
313
314
315		let current_time = self.system.current_time_ms();
316		let mut delta = current_time - self.last_time;
317
318
319		// reset to start frame if `draw` hasn't been called in more than a second
320		if delta > 1000 {
321			self.frame = 1;
322		}
323
324		// normalized steps by delta
325		while delta >= 50 {
326			self.last_time += 50;
327			delta -= 50;
328			self.frame += 1;
329			if self.frame > self.frame_count {
330				self.frame = 1;
331			}
332		}
333
334		// prepare next frame of the crank
335		if self.scale.as_u8() > 2 || self.frame > self.text_frame_count {
336			let index = if self.clockwise {
337				((self.frame - self.text_frame_count - 1) % CRANK_FRAME_COUNT) + 1
338			} else {
339				((CRANK_FRAME_COUNT - (self.frame - self.text_frame_count - 1)) % CRANK_FRAME_COUNT) + 1
340			} - 1;
341
342			if dirty || self.frame != last_frame {
343				dirty = true;
344
345				let frame = self.crank
346				                .get::<bitmap::api::Default>(index as _)
347				                .expect("missed frame");
348				let (fw, fh) = frame.size();
349
350				let x = self.bubble_pos.x + self.offset.x as i16 + (self.text_offset - fw as i16) / 2;
351				let y = self.bubble_pos.y + self.offset_correction_y() + (self.bubble_size.h as i16 - fh as i16) / 2;
352				self.crank_pos = Point::new(x, y);
353				self.crank_current = frame.into();
354			}
355		} else {
356			self.crank_current = None;
357		}
358
359		// is dirty:
360		// 0. if bitmaps just reloaded,
361		// 1. if frame changed,
362		// 2. if self.crank_current was None, but now is Some, and otherwise.
363		dirty || (crank_drawn != self.crank_current.is_some())
364	}
365
366
367	fn bounds(&self) -> PDRect {
368		PDRect { x: (self.bubble_pos.x + self.offset.x as i16) as _,
369		         y: (self.bubble_pos.y + self.offset.y as i16) as _,
370		         width: self.bubble_size.w as _,
371		         height: self.bubble_size.h as _ }
372	}
373}
374
375
376fn load_bubble_for_scale(scale: DisplayScale) -> Result<Bitmap<gfx::api::Default>, gfx::error::ApiError> {
377	let path = format!("ui/crank-ind/crank-notice-bubble-{}x", scale.as_u8());
378	Bitmap::load(path)
379}
380
381fn load_text_for_scale(scale: DisplayScale) -> Result<Bitmap<gfx::api::Default>, gfx::error::ApiError> {
382	let path = format!("ui/crank-ind/crank-notice-text-{}x", scale.as_u8());
383	Bitmap::load(path)
384}
385
386fn load_crank_for_scale(scale: DisplayScale)
387                        -> Result<BitmapTable<gfx::bitmap::table::api::Default>, gfx::error::ApiError> {
388	let path = format!("ui/crank-ind/crank-frames-{}x", scale.as_u8());
389	BitmapTable::load(path)
390}
391
392
393/// 2D point
394struct Point<T> {
395	x: T,
396	y: T,
397}
398
399impl<T> Point<T> {
400	const fn new(x: T, y: T) -> Point<T> { Self { x, y } }
401}
402
403/// 2D size
404struct Size<T> {
405	w: T,
406	h: T,
407}
408
409impl<T> Size<T> {
410	const fn new(w: T, h: T) -> Size<T> { Self { w, h } }
411}