use std::error::Error;
use std::sync::{Arc, Mutex};
use instant::Duration;
use once_cell::sync::Lazy;
use all_is_cubes::block::{space_to_blocks, AnimationHint, BlockAttributes, Resolution, AIR};
use all_is_cubes::cgmath::EuclideanSpace as _;
use all_is_cubes::character::{Character, CharacterChange};
use all_is_cubes::drawing::embedded_graphics::{
mono_font::MonoTextStyle,
prelude::Point,
text::{Alignment, Baseline, Text, TextStyleBuilder},
Drawable as _,
};
use all_is_cubes::listen::{FnListener, Gate, Listener};
use all_is_cubes::math::{GridAab, GridCoordinate, GridMatrix, GridPoint, GridVector};
use all_is_cubes::space::{Space, SpacePhysics, SpaceTransaction};
use all_is_cubes::time::Tick;
use all_is_cubes::universe::{URef, Universe};
use crate::vui::hud::{HudBlocks, HudFont};
use crate::vui::{LayoutRequest, Layoutable, Widget, WidgetController, WidgetTransaction};
static EMPTY_ARC_STR: Lazy<Arc<str>> = Lazy::new(|| "".into());
#[derive(Debug)]
pub(crate) struct TooltipState {
character: Option<URef<Character>>,
character_gate: Gate,
dirty_inventory: bool,
dirty_text: bool,
current_contents: TooltipContents,
last_inventory_message: TooltipContents,
age: Option<Duration>,
}
impl TooltipState {
pub(crate) fn bind_to_character(this_ref: &Arc<Mutex<Self>>, character: URef<Character>) {
let (gate, listener) =
FnListener::new(this_ref, move |this: &Mutex<Self>, change| match change {
CharacterChange::Inventory(_) | CharacterChange::Selections => {
if let Ok(mut this) = this.lock() {
this.dirty_inventory = true;
}
}
})
.gate();
character.read().unwrap().listen(listener);
{
let mut this = this_ref.lock().unwrap();
this.character = Some(character);
this.character_gate = gate;
this.dirty_inventory = true;
}
}
pub fn set_message(&mut self, text: Arc<str>) {
self.dirty_inventory = false;
self.set_contents(TooltipContents::Message(text))
}
fn set_contents(&mut self, contents: TooltipContents) {
self.dirty_text = true;
self.current_contents = contents;
self.age = Some(Duration::ZERO);
}
fn step(&mut self, hud_blocks: &HudBlocks, tick: Tick) -> Option<Arc<str>> {
if let Some(ref mut age) = self.age {
*age += tick.delta_t();
if *age > Duration::from_secs(1) {
self.set_contents(TooltipContents::Blanked);
self.age = None;
}
}
if self.dirty_inventory {
self.dirty_inventory = false;
if let Some(character_ref) = &self.character {
let character = character_ref.read().unwrap();
let selected_slot = character
.selected_slots()
.get(1)
.copied()
.unwrap_or(usize::MAX);
if let Some(tool) = character.inventory().slots.get(selected_slot).cloned() {
let new_text = tool
.icon(&hud_blocks.icons)
.evaluate()
.ok()
.map(|ev_block| ev_block.attributes.display_name.into_owned().into())
.unwrap_or_else(|| EMPTY_ARC_STR.clone());
let new_contents = TooltipContents::InventoryItem {
source_slot: selected_slot,
text: new_text,
};
if new_contents != self.last_inventory_message {
if self.last_inventory_message != TooltipContents::JustStartedExisting {
self.set_contents(new_contents.clone());
}
self.last_inventory_message = new_contents;
}
}
}
}
if self.dirty_text {
self.dirty_text = false;
Some(self.current_contents.text().clone())
} else {
None
}
}
}
impl Default for TooltipState {
fn default() -> Self {
Self {
character: None,
character_gate: Gate::default(),
dirty_inventory: false,
dirty_text: false,
current_contents: TooltipContents::JustStartedExisting,
last_inventory_message: TooltipContents::JustStartedExisting,
age: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum TooltipContents {
JustStartedExisting,
Blanked,
Message(Arc<str>),
InventoryItem {
source_slot: usize,
text: Arc<str>,
},
}
impl TooltipContents {
fn text(&self) -> &Arc<str> {
match self {
TooltipContents::JustStartedExisting | TooltipContents::Blanked => &EMPTY_ARC_STR,
TooltipContents::Message(m) => m,
TooltipContents::InventoryItem { text, .. } => text,
}
}
}
#[derive(Clone, Debug)]
pub(crate) struct Tooltip {
width_in_hud: GridCoordinate,
hud_blocks: Arc<HudBlocks>,
state: Arc<Mutex<TooltipState>>,
text_space: URef<Space>,
}
impl Tooltip {
const RESOLUTION: Resolution = Resolution::R16;
pub(crate) fn new(
state: Arc<Mutex<TooltipState>>,
hud_blocks: Arc<HudBlocks>,
universe: &mut Universe,
) -> Arc<Self> {
let width_in_hud = 25; let text_space = Space::builder(GridAab::from_lower_size(
GridPoint::origin(),
GridVector::new(
width_in_hud * GridCoordinate::from(Self::RESOLUTION),
GridCoordinate::from(Self::RESOLUTION),
2,
),
))
.physics(SpacePhysics::DEFAULT_FOR_BLOCK)
.build();
Arc::new(Self {
width_in_hud,
hud_blocks,
state,
text_space: universe.insert_anonymous(text_space),
})
}
}
impl Layoutable for Tooltip {
fn requirements(&self) -> LayoutRequest {
LayoutRequest {
minimum: GridVector::new(self.width_in_hud, 1, 1),
}
}
}
impl Widget for Tooltip {
fn controller(self: Arc<Self>, grant: &crate::vui::LayoutGrant) -> Box<dyn WidgetController> {
Box::new(TooltipController {
definition: self,
position: grant.bounds,
})
}
}
#[derive(Debug)]
struct TooltipController {
definition: Arc<Tooltip>,
position: GridAab,
}
impl WidgetController for TooltipController {
fn initialize(&mut self) -> Result<WidgetTransaction, crate::vui::InstallVuiError> {
let toolbar_text_blocks = space_to_blocks(
Tooltip::RESOLUTION,
BlockAttributes {
animation_hint: AnimationHint::CONTINUOUS,
..BlockAttributes::default()
},
self.definition.text_space.clone(),
)
.unwrap();
let mut txn = SpaceTransaction::default();
let origin: GridPoint = self.position.lower_bounds();
for i in 0..self.position.size().x {
txn.set_overwrite(
origin + i * GridVector::unit_x(),
toolbar_text_blocks[GridPoint::from_vec(i * GridVector::unit_x())].clone(),
);
}
Ok(txn)
}
fn step(&mut self, tick: Tick) -> Result<WidgetTransaction, Box<dyn Error + Send + Sync>> {
let text_update: Option<Arc<str>> = self
.definition
.state
.try_lock()
.ok()
.and_then(|mut state| state.step(&self.definition.hud_blocks, tick));
if let Some(text) = text_update {
self.definition.text_space.try_modify(|text_space| {
let bounds = text_space.bounds();
text_space.fill_uniform(bounds, &AIR).unwrap();
let text_obj = Text::with_text_style(
&text,
Point::new(bounds.size().x / 2, -1),
MonoTextStyle::new(&HudFont, &self.definition.hud_blocks.text),
TextStyleBuilder::new()
.baseline(Baseline::Bottom)
.alignment(Alignment::Center)
.build(),
);
text_obj.draw(&mut text_space.draw_target(GridMatrix::FLIP_Y))?;
Ok::<(), Box<dyn Error + Send + Sync>>(())
})??;
}
Ok(WidgetTransaction::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
use all_is_cubes::util::YieldProgress;
use futures_executor::block_on;
#[test]
fn tooltip_timeout_and_dirty_text() {
let mut universe = Universe::new();
let hud_blocks = &block_on(HudBlocks::new(&mut universe, YieldProgress::noop()));
let mut t = TooltipState::default();
assert_eq!(t.step(hud_blocks, Tick::from_seconds(0.5)), None);
assert_eq!(t.age, None);
t.set_message("Hello world".into());
assert_eq!(t.age, Some(Duration::ZERO));
assert_eq!(
t.step(hud_blocks, Tick::from_seconds(0.25)),
Some("Hello world".into())
);
assert_eq!(t.step(hud_blocks, Tick::from_seconds(0.25)), None);
assert_eq!(t.age, Some(Duration::from_millis(500)));
assert_eq!(
t.step(hud_blocks, Tick::from_seconds(0.501)),
Some("".into())
);
assert_eq!(t.age, None);
assert_eq!(t.step(hud_blocks, Tick::from_seconds(2.00)), None);
}
}