widgetry/mapspace/
world.rs

1use std::collections::HashMap;
2use std::fmt::Debug;
3use std::hash::Hash;
4
5use geom::{Bounds, Circle, Distance, Polygon, Pt2D, QuadTree};
6
7use crate::mapspace::{ToggleZoomed, ToggleZoomedBuilder};
8use crate::{Color, Drawable, EventCtx, GeomBatch, GfxCtx, MultiKey, RewriteColor, Text};
9
10// TODO Tests...
11// - start drag in screenspace, release in map
12// - start drag in mapspace, release in screen
13// - reset hovering when we go out of screenspace
14// - start dragging one object, and while dragging, hover on top of other objects
15
16/// A `World` manages objects that exist in "map-space", the zoomable and pannable canvas. These
17/// objects can be drawn, hovered on, clicked, dragged, etc.
18pub struct World<ID: ObjectID> {
19    // TODO Hashing may be too slow in some cases
20    objects: HashMap<ID, Object<ID>>,
21    quadtree: QuadTree<ID>,
22
23    draw_master_batches: Vec<ToggleZoomed>,
24
25    hovering: Option<ID>,
26    // Only for ColorHitbox cases
27    draw_hovering: Option<Drawable>,
28    // If we're currently dragging, where was the cursor during the last movement, and has the
29    // cursor moved since starting the drag?
30    dragging_from: Option<(Pt2D, bool)>,
31}
32
33/// The result of a `World` handling an event
34#[derive(Clone)]
35pub enum WorldOutcome<ID: ObjectID> {
36    /// A left click occurred while not hovering on any object
37    ClickedFreeSpace(Pt2D),
38    /// An object is being dragged. The given offsets are relative to the previous dragging event.
39    /// The current position of the cursor is included. If you're dragging a large object, applying
40    /// the offset will likely feel more natural than centering on the cursor.
41    Dragging {
42        obj: ID,
43        dx: f64,
44        dy: f64,
45        cursor: Pt2D,
46    },
47    /// While hovering on an object with a defined hotkey, that key was pressed.
48    Keypress(&'static str, ID),
49    /// A hoverable object was clicked
50    ClickedObject(ID),
51    /// The object being hovered on changed from (something before, to something after). Note this
52    /// transition may also occur outside of `event` -- such as during `delete` or `initialize_hover`.
53    ///
54    /// TODO Bug in the map_editor: If you delete one object, then the caller does initialize_hover
55    /// and we immediately wind up on another road beneath, we don't detect this and start showing
56    /// road points.
57    HoverChanged(Option<ID>, Option<ID>),
58    /// Nothing interesting happened
59    Nothing,
60}
61
62impl<I: ObjectID> WorldOutcome<I> {
63    /// If the outcome references some ID, transform it to another type. This is useful when some
64    /// component owns a World that contains a few different types of objects, some of which are
65    /// managed by another component that only cares about its IDs.
66    pub fn maybe_map_id<O: ObjectID, F: Fn(I) -> Option<O>>(self, f: F) -> Option<WorldOutcome<O>> {
67        match self {
68            WorldOutcome::ClickedFreeSpace(pt) => Some(WorldOutcome::ClickedFreeSpace(pt)),
69            WorldOutcome::Dragging {
70                obj,
71                dx,
72                dy,
73                cursor,
74            } => Some(WorldOutcome::Dragging {
75                obj: f(obj)?,
76                dx,
77                dy,
78                cursor,
79            }),
80            WorldOutcome::Keypress(action, id) => Some(WorldOutcome::Keypress(action, f(id)?)),
81            WorldOutcome::ClickedObject(id) => Some(WorldOutcome::ClickedObject(f(id)?)),
82            WorldOutcome::HoverChanged(before, after) => {
83                // If f returns None, bail out. But preserve None if before or after originally was
84                // that.
85                let before = match before {
86                    Some(x) => Some(f(x)?),
87                    None => None,
88                };
89                let after = match after {
90                    Some(x) => Some(f(x)?),
91                    None => None,
92                };
93                Some(WorldOutcome::HoverChanged(before, after))
94            }
95            WorldOutcome::Nothing => Some(WorldOutcome::Nothing),
96        }
97    }
98}
99
100/// Objects in a `World` are uniquely identified by this caller-specified type
101pub trait ObjectID: Clone + Copy + Debug + Eq + Hash {}
102
103/// This provides a builder API for adding objects to a `World`.
104pub struct ObjectBuilder<'a, ID: ObjectID> {
105    world: &'a mut World<ID>,
106
107    id: ID,
108    hitboxes: Vec<Polygon>,
109    zorder: usize,
110    draw_normal: Option<ToggleZoomedBuilder>,
111    draw_hover: Option<HoverBuilder>,
112    tooltip: Option<Text>,
113    clickable: bool,
114    draggable: bool,
115    keybindings: Vec<(MultiKey, &'static str)>,
116}
117
118enum HoverBuilder {
119    ColorHitbox(Color),
120    Custom(ToggleZoomedBuilder),
121}
122
123impl<'a, ID: ObjectID> ObjectBuilder<'a, ID> {
124    /// Specifies the geometry of the object. Required.
125    pub fn hitbox(mut self, polygon: Polygon) -> Self {
126        assert!(self.hitboxes.is_empty(), "called hitbox twice");
127        self.hitboxes = vec![polygon];
128        self
129    }
130
131    /// Specifies the geometry of the object as a multipolygon.
132    pub fn hitboxes(mut self, polygons: Vec<Polygon>) -> Self {
133        assert!(self.hitboxes.is_empty(), "called hitbox twice");
134        assert!(!polygons.is_empty(), "not specifying any hitboxes");
135        self.hitboxes = polygons;
136        self
137    }
138
139    /// Provides ordering for overlapping objects. Higher values are "on top" of lower values.
140    pub fn zorder(mut self, zorder: usize) -> Self {
141        assert!(self.zorder == 0, "called zorder twice");
142        self.zorder = zorder;
143        self
144    }
145
146    /// Specifies how to draw this object normally (while not hovering on it)
147    pub fn draw<I: Into<ToggleZoomedBuilder>>(mut self, normal: I) -> Self {
148        assert!(
149            self.draw_normal.is_none(),
150            "already specified how to draw normally"
151        );
152        self.draw_normal = Some(normal.into());
153        self
154    }
155
156    /// Draw the object by coloring its hitbox
157    pub fn draw_color(self, color: Color) -> Self {
158        assert!(!self.hitboxes.is_empty(), "call hitbox first");
159        let mut batch = GeomBatch::new();
160        batch.extend(color, self.hitboxes.clone());
161        self.draw(batch)
162    }
163
164    /// Draw the object by coloring its hitbox, only when unzoomed. Show nothing when zoomed.
165    pub fn draw_color_unzoomed(self, color: Color) -> Self {
166        assert!(!self.hitboxes.is_empty(), "call hitbox first");
167        let mut draw = ToggleZoomed::builder();
168        draw.unzoomed.extend(color, self.hitboxes.clone());
169        self.draw(draw)
170    }
171
172    /// Indicate that an object doesn't need to be drawn individually. A call to
173    /// `draw_master_batch` covers it.
174    pub fn drawn_in_master_batch(self) -> Self {
175        assert!(
176            self.draw_normal.is_none(),
177            "object is already drawn normally"
178        );
179        self.draw(GeomBatch::new())
180    }
181
182    /// Specifies how to draw the object while the cursor is hovering on it. Note that an object
183    /// isn't considered hoverable unless this is specified!
184    pub fn draw_hovered<I: Into<ToggleZoomedBuilder>>(mut self, hovered: I) -> Self {
185        assert!(
186            self.draw_hover.is_none(),
187            "already specified how to draw hovered"
188        );
189        self.draw_hover = Some(HoverBuilder::Custom(hovered.into()));
190        self
191    }
192
193    /// Draw the object in a hovered state by transforming the normal drawing.
194    pub fn draw_hover_rewrite(self, rewrite: RewriteColor) -> Self {
195        let hovered = self
196            .draw_normal
197            .clone()
198            .expect("first specify how to draw normally")
199            .color(rewrite);
200        self.draw_hovered(hovered)
201    }
202
203    /// Draw the object in a hovered state by changing the alpha value of the normal drawing.
204    pub fn hover_alpha(self, alpha: f32) -> Self {
205        self.draw_hover_rewrite(RewriteColor::ChangeAlpha(alpha))
206    }
207
208    /// Draw the object in a hovered state by adding an outline to the normal drawing. The
209    /// specified `color` and `thickness` will be used when unzoomed. For the zoomed view, the
210    /// color's opacity and the thickness will be halved.
211    pub fn hover_outline(self, color: Color, thickness: Distance) -> Self {
212        let mut draw = self
213            .draw_normal
214            .clone()
215            .expect("first specify how to draw normally")
216            .draw_differently_zoomed();
217        let mut unzoomed = Vec::new();
218        let mut zoomed = Vec::new();
219        assert!(!self.hitboxes.is_empty(), "call hitbox first");
220        for polygon in &self.hitboxes {
221            unzoomed.push(polygon.to_outline(thickness));
222            zoomed.push(polygon.to_outline(thickness / 2.0));
223        }
224        if unzoomed.len() == zoomed.len() && unzoomed.len() == self.hitboxes.len() {
225            draw.unzoomed.extend(color, unzoomed);
226            draw.zoomed.extend(color.multiply_alpha(0.5), zoomed);
227        } else {
228            warn!(
229                "Can't hover_outline for {:?}. Falling back to a colored polygon",
230                self.id
231            );
232            let mut batch = GeomBatch::new();
233            batch.extend(color.multiply_alpha(0.5), self.hitboxes.clone());
234            draw = batch.into();
235        }
236        self.draw_hovered(draw)
237    }
238
239    /// Draw the object in a hovered state by coloring its hitbox. Useful when
240    /// `drawn_in_master_batch` is used and there's no normal drawn polygon.
241    pub fn hover_color(mut self, color: Color) -> Self {
242        assert!(!self.hitboxes.is_empty(), "call hitbox first");
243        self.draw_hover = Some(HoverBuilder::ColorHitbox(color));
244        self
245    }
246
247    /// Mark that an object is hoverable, but don't actually draw anything while hovering on it
248    pub fn invisibly_hoverable(self) -> Self {
249        self.draw_hovered(GeomBatch::new())
250    }
251
252    /// Maybe draw a tooltip while hovering over this object.
253    pub fn maybe_tooltip(self, txt: Option<Text>) -> Self {
254        if let Some(txt) = txt {
255            self.tooltip(txt)
256        } else {
257            self
258        }
259    }
260
261    /// Draw a tooltip while hovering over this object.
262    pub fn tooltip(mut self, txt: Text) -> Self {
263        assert!(self.tooltip.is_none(), "already specified tooltip");
264        // TODO Or should this implicitly mark the object as hoverable? Is it weird to base this
265        // off drawing?
266        assert!(
267            self.draw_hover.is_some(),
268            "first specify how to draw hovered"
269        );
270        self.tooltip = Some(txt);
271        self
272    }
273
274    /// Mark the object as clickable. `WorldOutcome::ClickedObject` will be fired.
275    pub fn clickable(mut self) -> Self {
276        assert!(!self.clickable, "called clickable twice");
277        self.clickable = true;
278        self
279    }
280
281    /// Mark the object as clickable or not. `WorldOutcome::ClickedObject` will be fired.
282    pub fn set_clickable(mut self, clickable: bool) -> Self {
283        self.clickable = clickable;
284        self
285    }
286
287    /// Mark the object as draggable. The user can hover on this object, then click and drag it.
288    /// `WorldOutcome::Dragging` events will be fired.
289    ///
290    /// Note that dragging an object doesn't transform it at all (for example, by translating its
291    /// hitbox). The caller is responsible for doing that.
292    pub fn draggable(mut self) -> Self {
293        assert!(!self.draggable, "called draggable twice");
294        self.draggable = true;
295        self
296    }
297
298    /// While the user hovers over this object, they can press a key to perform the specified
299    /// action. `WorldOutcome::Keypress` will be fired.
300    pub fn hotkey<I: Into<MultiKey>>(mut self, key: I, action: &'static str) -> Self {
301        // TODO Check for duplicate keybindings
302        self.keybindings.push((key.into(), action));
303        self
304    }
305
306    /// Finalize the object, adding it to the `World`.
307    pub fn build(mut self, ctx: &EventCtx) {
308        assert!(!self.hitboxes.is_empty(), "didn't specify hitbox");
309        let bounds = Bounds::from_polygons(&self.hitboxes);
310        self.world.quadtree.insert_with_box(self.id, bounds);
311
312        self.world.objects.insert(
313            self.id,
314            Object {
315                _id: self.id,
316                hitboxes: self.hitboxes,
317                zorder: self.zorder,
318                draw_normal: self
319                    .draw_normal
320                    .expect("didn't specify how to draw normally")
321                    .build(ctx),
322                draw_hover: self.draw_hover.take().map(|draw| match draw {
323                    HoverBuilder::Custom(draw) => DrawHover::Custom(draw.build(ctx)),
324                    HoverBuilder::ColorHitbox(color) => DrawHover::ColorHitbox(color),
325                }),
326                tooltip: self.tooltip,
327                clickable: self.clickable,
328                draggable: self.draggable,
329                keybindings: self.keybindings,
330            },
331        );
332    }
333}
334
335struct Object<ID: ObjectID> {
336    _id: ID,
337    hitboxes: Vec<Polygon>,
338    zorder: usize,
339    draw_normal: ToggleZoomed,
340    draw_hover: Option<DrawHover>,
341    tooltip: Option<Text>,
342    clickable: bool,
343    draggable: bool,
344    // TODO How should we communicate these keypresses are possible? Something standard, like
345    // button tooltips?
346    keybindings: Vec<(MultiKey, &'static str)>,
347}
348
349enum DrawHover {
350    ColorHitbox(Color),
351    Custom(ToggleZoomed),
352}
353
354impl<ID: ObjectID> World<ID> {
355    /// Creates an empty `World`
356    pub fn new() -> World<ID> {
357        World {
358            objects: HashMap::new(),
359            quadtree: QuadTree::new(),
360
361            draw_master_batches: Vec::new(),
362
363            hovering: None,
364            draw_hovering: None,
365            dragging_from: None,
366        }
367    }
368
369    /// Start adding an object to the `World`. The caller should specify the object with methods on
370    /// `ObjectBuilder`, then call `build`.
371    pub fn add(&mut self, id: ID) -> ObjectBuilder<'_, ID> {
372        assert!(!self.objects.contains_key(&id), "duplicate object added");
373        ObjectBuilder {
374            world: self,
375
376            id,
377            hitboxes: Vec::new(),
378            zorder: 0,
379            draw_normal: None,
380            draw_hover: None,
381            tooltip: None,
382            clickable: false,
383            draggable: false,
384            keybindings: Vec::new(),
385        }
386    }
387
388    /// Delete an object. Not idempotent -- this will panic if the object doesn't exist. Will panic
389    /// if the object is deleted in the middle of being dragged.
390    pub fn delete(&mut self, id: ID) {
391        if self.hovering == Some(id) {
392            self.hovering = None;
393            self.draw_hovering = None;
394            if self.dragging_from.is_some() {
395                panic!("Can't delete {:?} mid-drag", id);
396            }
397        }
398
399        self.delete_before_replacement(id);
400    }
401
402    /// Delete an object, with the promise to recreate it with the same ID before the next call to
403    /// `event`. This may be called while the object is being hovered on or dragged.
404    pub fn delete_before_replacement(&mut self, id: ID) {
405        if self.objects.remove(&id).is_some() {
406            if self.quadtree.remove(id).is_none() {
407                // This can happen for objects that're out-of-bounds. One example is intersections
408                // in map_editor.
409                warn!("{:?} wasn't in the quadtree", id);
410            }
411        } else {
412            panic!("Can't delete {:?}; it's not in the World", id);
413        }
414    }
415
416    /// Like delete, but doesn't crash if the object doesn't exist
417    pub fn maybe_delete(&mut self, id: ID) {
418        if self.hovering == Some(id) {
419            self.hovering = None;
420            self.draw_hovering = None;
421            if self.dragging_from.is_some() {
422                panic!("Can't delete {:?} mid-drag", id);
423            }
424        }
425
426        if self.objects.remove(&id).is_some() {
427            if self.quadtree.remove(id).is_none() {
428                // This can happen for objects that're out-of-bounds. One example is intersections
429                // in map_editor.
430                warn!("{:?} wasn't in the quadtree", id);
431            }
432        }
433    }
434
435    /// After adding all objects to a `World`, call this to initially detect if the cursor is
436    /// hovering on an object. This may also be called after adding or deleting objects to
437    /// immediately recalculate hover before the mouse moves.
438    // TODO Maybe we should automatically do this after mutations? Except we don't want to in the
439    // middle of a bulk operation, like initial setup or a many-step mutation. So maybe the caller
440    // really should handle it.
441    pub fn initialize_hover(&mut self, ctx: &EventCtx) {
442        self.hovering = ctx
443            .canvas
444            .get_cursor_in_map_space()
445            .and_then(|cursor| self.calculate_hover(cursor));
446        self.redraw_hovering(ctx);
447    }
448
449    /// Forcibly reset the hovering state to empty. This is a necessary hack when launching a new
450    /// state that uses `DrawBaselayer::PreviousState` and has tooltips.
451    pub fn hack_unset_hovering(&mut self) {
452        self.hovering = None;
453        self.draw_hovering = None;
454    }
455
456    /// If a drag event causes the world to be totally rebuilt, call this with the previous world
457    /// to preserve the ongoing drag.
458    ///
459    /// This should be called after `initialize_hover`.
460    ///
461    /// Important: the rebuilt world must include the same object ID that's currently being dragged
462    /// from the previous world.
463    pub fn rebuilt_during_drag(&mut self, ctx: &EventCtx, prev_world: &World<ID>) {
464        if prev_world.dragging_from.is_some() {
465            self.dragging_from = prev_world.dragging_from;
466            self.hovering = prev_world.hovering;
467            assert!(self.objects.contains_key(self.hovering.as_ref().unwrap()));
468            self.redraw_hovering(ctx);
469        }
470    }
471
472    /// Draw something underneath all objects. This is useful for performance, when a large number
473    /// of objects never change appearance.
474    pub fn draw_master_batch<I: Into<ToggleZoomedBuilder>>(&mut self, ctx: &EventCtx, draw: I) {
475        self.draw_master_batches.push(draw.into().build(ctx));
476    }
477
478    /// Like `draw_master_batch`, but for already-built objects.
479    pub fn draw_master_batch_built(&mut self, draw: ToggleZoomed) {
480        self.draw_master_batches.push(draw);
481    }
482
483    /// Let objects in the world respond to something happening.
484    pub fn event(&mut self, ctx: &mut EventCtx) -> WorldOutcome<ID> {
485        if let Some((drag_from, moved)) = self.dragging_from {
486            if ctx.input.left_mouse_button_released() {
487                self.dragging_from = None;
488                // For objects that're both clickable and draggable, we don't know what the user is
489                // doing until they release the mouse!
490                if !moved && self.objects[&self.hovering.unwrap()].clickable {
491                    return WorldOutcome::ClickedObject(self.hovering.unwrap());
492                }
493
494                let before = self.hovering;
495                self.hovering = ctx
496                    .canvas
497                    .get_cursor_in_map_space()
498                    .and_then(|cursor| self.calculate_hover(cursor));
499                return if before == self.hovering {
500                    WorldOutcome::Nothing
501                } else {
502                    self.redraw_hovering(ctx);
503                    WorldOutcome::HoverChanged(before, self.hovering)
504                };
505            }
506            // Allow zooming, but not panning, while dragging
507            if let Some((_, dy)) = ctx.input.get_mouse_scroll() {
508                ctx.canvas.zoom(dy, ctx.canvas.get_cursor());
509            }
510
511            if ctx.redo_mouseover() {
512                if let Some(cursor) = ctx.canvas.get_cursor_in_map_space() {
513                    let dx = cursor.x() - drag_from.x();
514                    let dy = cursor.y() - drag_from.y();
515                    self.dragging_from = Some((cursor, true));
516                    return WorldOutcome::Dragging {
517                        obj: self.hovering.unwrap(),
518                        dx,
519                        dy,
520                        cursor,
521                    };
522                }
523            }
524
525            return WorldOutcome::Nothing;
526        }
527
528        let cursor = if let Some(pt) = ctx.canvas.get_cursor_in_map_space() {
529            pt
530        } else {
531            let before = self.hovering.take();
532            return if before.is_some() {
533                WorldOutcome::HoverChanged(before, None)
534            } else {
535                WorldOutcome::Nothing
536            };
537        };
538
539        // Possibly recalculate hovering
540        let mut neutral_outcome = WorldOutcome::Nothing;
541        if ctx.redo_mouseover() {
542            let before = self.hovering;
543            self.hovering = self.calculate_hover(cursor);
544            if before != self.hovering {
545                self.redraw_hovering(ctx);
546                neutral_outcome = WorldOutcome::HoverChanged(before, self.hovering);
547            }
548        }
549
550        // If we're hovering on a draggable thing, only allow zooming, not panning
551        let mut allow_panning = true;
552        if let Some(id) = self.hovering {
553            let obj = &self.objects[&id];
554
555            // For objects both clickable and draggable, the branch below will win, and we'll
556            // detect a normal click elsewhere.
557            if obj.clickable && ctx.normal_left_click() {
558                return WorldOutcome::ClickedObject(id);
559            }
560
561            if obj.draggable {
562                allow_panning = false;
563                if ctx.input.left_mouse_button_pressed() {
564                    self.dragging_from = Some((cursor, false));
565                    return neutral_outcome;
566                }
567            }
568
569            for (key, action) in &obj.keybindings {
570                if ctx.input.pressed(key.clone()) {
571                    return WorldOutcome::Keypress(action, id);
572                }
573            }
574        }
575
576        if allow_panning {
577            ctx.canvas_movement();
578
579            if self.hovering.is_none() && ctx.normal_left_click() {
580                return WorldOutcome::ClickedFreeSpace(cursor);
581            }
582        } else if let Some((_, dy)) = ctx.input.get_mouse_scroll() {
583            ctx.canvas.zoom(dy, ctx.canvas.get_cursor());
584        }
585
586        neutral_outcome
587    }
588
589    fn calculate_hover(&self, cursor: Pt2D) -> Option<ID> {
590        let mut objects = Vec::new();
591        for id in self.quadtree.query_bbox(
592            // Maybe worth tuning. Since we do contains_pt below, it doesn't matter if this is too
593            // big; just a performance impact possibly.
594            Circle::new(cursor, Distance::meters(3.0)).get_bounds(),
595        ) {
596            objects.push(id);
597        }
598        objects.sort_by_key(|id| self.objects[id].zorder);
599        objects.reverse();
600
601        for id in objects {
602            let obj = &self.objects[&id];
603            if obj.draw_hover.is_some() && obj.hitboxes.iter().any(|poly| poly.contains_pt(cursor))
604            {
605                return Some(id);
606            }
607        }
608        None
609    }
610
611    /// Draw objects in the world that're currently visible.
612    pub fn draw(&self, g: &mut GfxCtx) {
613        // Always draw master batches first
614        for draw in &self.draw_master_batches {
615            draw.draw(g);
616        }
617
618        let mut objects = Vec::new();
619        for id in self.quadtree.query_bbox(g.get_screen_bounds()) {
620            objects.push(id);
621        }
622        objects.sort_by_key(|id| self.objects[id].zorder);
623
624        for id in objects {
625            let mut drawn = false;
626            let obj = &self.objects[&id];
627            if Some(id) == self.hovering {
628                if let Some(ref draw) = obj.draw_hover {
629                    match draw {
630                        DrawHover::Custom(draw) => draw.draw(g),
631                        DrawHover::ColorHitbox(_) => self.draw_hovering.as_ref().unwrap().draw(g),
632                    }
633                    drawn = true;
634                }
635                if let Some(ref txt) = obj.tooltip {
636                    g.draw_mouse_tooltip(txt.clone());
637                }
638            }
639            if !drawn {
640                obj.draw_normal.draw(g);
641            }
642        }
643    }
644
645    /// Returns the object currently hovered on.
646    pub fn get_hovering(&self) -> Option<ID> {
647        self.hovering
648    }
649
650    /// Change an object's tooltip. Returns true for success, false if the object didn't exist.
651    pub fn override_tooltip(&mut self, id: &ID, tooltip: Option<Text>) -> bool {
652        if let Some(obj) = self.objects.get_mut(id) {
653            obj.tooltip = tooltip;
654            true
655        } else {
656            false
657        }
658    }
659
660    /// Calculate the object currently underneath the cursor. This should only be used when the
661    /// `World` is not being actively updated by calling `event`. If another state temporarily
662    /// needs to disable most interactions with objects, it can poll this instead.
663    pub fn calculate_hovering(&self, ctx: &EventCtx) -> Option<ID> {
664        // TODO Seems expensive! Maybe instead set some kind of "locked" mode and disable
665        // everything except hovering?
666        ctx.canvas
667            .get_cursor_in_map_space()
668            .and_then(|cursor| self.calculate_hover(cursor))
669    }
670
671    /// If an object is currently being hovered on, return its keybindings. This should be used to
672    /// describe interactions; to detect the keypresses, listen for `WorldOutcome::Keypress`.
673    pub fn get_hovered_keybindings(&self) -> Option<&Vec<(MultiKey, &'static str)>> {
674        Some(&self.objects[&self.hovering?].keybindings)
675    }
676
677    fn redraw_hovering(&mut self, ctx: &EventCtx) {
678        self.draw_hovering = None;
679        if let Some(id) = self.hovering {
680            let obj = &self.objects[&id];
681            if let Some(DrawHover::ColorHitbox(color)) = obj.draw_hover {
682                let mut batch = GeomBatch::new();
683                batch.extend(color, obj.hitboxes.clone());
684                self.draw_hovering = Some(batch.upload(ctx));
685            }
686        }
687    }
688}
689
690/// If you don't ever need to refer to objects in a `World`, you can auto-assign dummy IDs.
691#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
692pub struct DummyID(usize);
693impl ObjectID for DummyID {}
694
695impl World<DummyID> {
696    /// Begin adding an unnamed object to the `World`.
697    ///
698    /// Note: You must call `build` on this object before calling `add_unnamed` again. Otherwise,
699    /// the object IDs will collide.
700    ///
701    /// TODO This will break when objects are deleted!
702    pub fn add_unnamed(&mut self) -> ObjectBuilder<'_, DummyID> {
703        self.add(DummyID(self.objects.len()))
704    }
705}