use std::rc::Rc; use gpui::{ App, AppContext as _, Bounds, ElementId, EmptyView, Entity, EntityId, InteractiveElement, IntoElement, ParentElement, Pixels, RenderOnce, StatefulInteractiveElement, Styled, Window, canvas, div, fill, point, size, }; use super::{ plane::OnViewport, view::{Frame, Viewport}, }; use crate::theme::{ActiveTheme, Radius}; /// A drag on a minimap, and whose map it is. struct Steer { owner: EntityId, } /// Where a map of `whole` shown in a box of `size` puts things: its scale and the offset that centers it. pub(crate) fn fitted(whole: Frame, (w, h): (f32, f32)) -> (f32, (f32, f32)) { let scale = (w / whole.w.max(1.0)).min(h / whole.h.max(1.0)); let offset = ((w - whole.w * scale) / 2.0, (h - whole.h * scale) / 2.0); (scale, offset) } /// A small map of what lies on a canvas and the part in view; a press or a drag on it centers the view there. #[derive(IntoElement)] pub struct MiniMap { id: ElementId, frames: Vec, viewport: Viewport, view: (f32, f32), on_viewport: Option, } impl MiniMap { /// The frames of what lies on the canvas, the viewport, and the size of the canvas's view in pixels. pub fn new( id: impl Into, frames: impl IntoIterator, viewport: Viewport, view: (f32, f32), ) -> Self { Self { id: id.into(), frames: frames.into_iter().collect(), viewport, view, on_viewport: None, } } pub fn on_viewport( mut self, handler: impl Fn(Viewport, &mut Window, &mut App) + 'static, ) -> Self { self.on_viewport = Some(Rc::new(handler)); self } } impl RenderOnce for MiniMap { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let bounds: Entity> = window.use_keyed_state((self.id.clone(), "box"), cx, |_, _| Bounds::default()); let theme = cx.theme(); let (colors, sizes, rem) = (theme.colors.clone(), theme.canvas(), window.rem_size()); let (w, h) = ( f32::from(sizes.map.0.to_pixels(rem)), f32::from(sizes.map.1.to_pixels(rem)), ); let (left, top) = self.viewport.to_canvas((0.0, 0.0)); let seen = Frame::new( left, top, self.view.0 / self.viewport.zoom, self.view.1 / self.viewport.zoom, ); let whole = self .frames .iter() .fold(seen, |whole, frame| whole.union(frame)); let (scale, (dx, dy)) = fitted(whole, (w, h)); let map = move |frame: &Frame| { Frame::new( dx + (frame.x - whole.x) * scale, dy + (frame.y - whole.y) * scale, frame.w * scale, frame.h * scale, ) }; let steer = { let (bounds, viewport, view, on_viewport) = ( bounds.clone(), self.viewport, self.view, self.on_viewport.clone(), ); move |at: gpui::Point, window: &mut Window, cx: &mut App| { let origin = bounds.read(cx).origin; let (x, y) = (f32::from(at.x - origin.x), f32::from(at.y - origin.y)); let (cx_, cy_) = (whole.x + (x - dx) / scale, whole.y + (y - dy) / scale); let next = Viewport::new( cx_ - view.0 / 2.0 / viewport.zoom, cy_ - view.1 / 2.0 / viewport.zoom, viewport.zoom, ); log::info!("minimap: view at {:.0}, {:.0}", next.x, next.y); if let Some(on_viewport) = &on_viewport { on_viewport(next, window, cx); } } }; let frames: Vec = self.frames.iter().map(map).collect(); let lens = map(&seen); let (palette, hairline) = (colors.clone(), sizes.hairline); let owner = bounds.entity_id(); let (pressed, moved, measured) = (steer.clone(), steer, bounds); div() .id(self.id) .relative() .w(sizes.map.0) .h(sizes.map.1) .rounded(theme.radius(Radius::Md)) .border_1() .border_color(colors.border) .bg(colors.surface) .overflow_hidden() .on_mouse_down(gpui::MouseButton::Left, move |event, window, cx| { pressed(event.position, window, cx) }) .on_drag(Steer { owner }, |_, _, _, cx| cx.new(|_| EmptyView)) .on_drag_move::(move |event, window, cx| { if event.drag(cx).owner == owner { moved(event.event.position, window, cx) } }) .child( canvas( move |bounds, _, cx| { if *measured.read(cx) != bounds { measured.update(cx, |held, _| *held = bounds); } }, move |bounds, _, window, _| { let place = |frame: &Frame| { Bounds::new( bounds.origin + point(Pixels::from(frame.x), Pixels::from(frame.y)), size(Pixels::from(frame.w), Pixels::from(frame.h)), ) }; for frame in &frames { window.paint_quad(fill(place(frame), palette.border_strong)); } window.paint_quad( fill(place(&lens), palette.fg.alpha(0.06)) .border_widths(hairline) .border_color(palette.fg_muted), ); }, ) .absolute() .top_0() .left_0() .size_full(), ) } } #[cfg(test)] mod tests { use super::{Frame, fitted}; #[test] fn a_map_fits_the_whole_and_centers_it() { let (scale, offset) = fitted(Frame::new(0.0, 0.0, 400.0, 100.0), (200.0, 140.0)); assert_eq!(scale, 0.5); assert_eq!(offset, (0.0, 45.0)); } }