use std::fmt::Debug; use figures::units::{Px, UPx}; use figures::{Point, Size}; use intentional::Cast; use kludgine::app::winit::event::{ElementState, MouseScrollDelta, TouchPhase}; use kludgine::app::winit::window::CursorIcon; use kludgine::tilemap; use kludgine::tilemap::TileMapFocus; use crate::context::{EventContext, GraphicsContext, LayoutContext, Trackable}; use crate::tick::Tick; use crate::value::{Dynamic, IntoValue, Value}; use crate::widget::{EventHandling, Widget, HANDLED, IGNORED}; use crate::window::{DeviceId, KeyEvent}; use crate::ConstraintLimit; /// A layered tile-based 2d game surface. #[derive(Debug)] #[must_use] pub struct TileMap { layers: Value, focus: Value, zoom: f32, tick: Option, } impl TileMap { fn construct(layers: Value) -> Self { Self { layers, focus: Value::default(), zoom: 1., tick: None, } } /// Returns a new tilemap that contains dynamic layers. pub fn dynamic(layers: Dynamic) -> Self { Self::construct(Value::Dynamic(layers)) } /// Returns a new tilemap that renders `layers`. pub fn new(layers: Layers) -> Self { Self::construct(Value::Constant(layers)) } /// Sets the camera's focus and returns self. /// /// The tilemap will ensure that `focus` is centered. // TODO how do we allow the camera to "lag" for juice effects? pub fn focus_on(mut self, focus: impl IntoValue) -> Self { self.focus = focus.into_value(); self } /// Associates a [`Tick`] with this widget and returns self. pub fn tick(mut self, tick: Tick) -> Self { self.tick = Some(tick); self } } impl Widget for TileMap where Layers: tilemap::Layers, { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_>) { let focus = self.focus.get(); // TODO this needs to be updated to support being placed in side of a scroll view. let redraw_after = match &mut self.layers { Value::Constant(layers) => tilemap::draw( layers, focus, self.zoom, context.elapsed(), context.gfx.inner_graphics(), ), Value::Dynamic(layers) => { let mut layers = layers.lock(); layers.prevent_notifications(); tilemap::draw( &mut *layers, focus, self.zoom, context.elapsed(), context.gfx.inner_graphics(), ) } }; context.draw_focus_ring(); if let Some(tick) = &self.tick { // When we are driven by a tick, we ignore all other sources of // refreshes. tick.rendered(context); } else { if let Some(redraw_after) = redraw_after { context.redraw_in(redraw_after); } self.focus.redraw_when_changed(context); self.layers.redraw_when_changed(context); } } fn accept_focus(&mut self, _context: &mut EventContext<'_>) -> bool { true } fn hit_test( &mut self, _location: figures::Point, _context: &mut EventContext<'_>, ) -> bool { true } fn layout( &mut self, available_space: Size, _context: &mut LayoutContext<'_, '_, '_, '_>, ) -> Size { Size::new(available_space.width.max(), available_space.height.max()) } fn mouse_wheel( &mut self, _device_id: DeviceId, delta: MouseScrollDelta, _phase: TouchPhase, context: &mut EventContext<'_>, ) -> EventHandling { let amount = match delta { MouseScrollDelta::LineDelta(_, lines) => lines, MouseScrollDelta::PixelDelta(px) => px.y.cast::() / 16.0, }; self.zoom += self.zoom * 0.1 * amount; context.set_needs_redraw(); HANDLED } fn hover(&mut self, local: Point, context: &mut EventContext<'_>) -> Option { if let Some(tick) = &self.tick { let Some(size) = context.last_layout().map(|rect| rect.size) else { return None; }; let world = tilemap::translate_coordinates(local, context.kludgine.scale(), self.zoom, size); let offset = self .layers .map(|layers| self.focus.get().world_coordinate(layers)); tick.set_cursor_position(Some(world + offset)); } None } fn unhover(&mut self, _context: &mut EventContext<'_>) { if let Some(tick) = &self.tick { tick.set_cursor_position(None); } } fn keyboard_input( &mut self, _device_id: DeviceId, input: KeyEvent, _is_synthetic: bool, _context: &mut EventContext<'_>, ) -> EventHandling { if let Some(tick) = &self.tick { tick.key_input(&input)?; } IGNORED } fn mouse_down( &mut self, _location: Point, _device_id: DeviceId, button: kludgine::app::winit::event::MouseButton, context: &mut EventContext<'_>, ) -> EventHandling { if let Some(tick) = &self.tick { tick.mouse_button(button, ElementState::Pressed); context.focus(); HANDLED } else { IGNORED } } fn mouse_up( &mut self, _location: Option>, _device_id: DeviceId, button: kludgine::app::winit::event::MouseButton, _context: &mut EventContext<'_>, ) { if let Some(tick) = &self.tick { tick.mouse_button(button, ElementState::Released); } } }