app.rs (7121B)
1 use std::sync::Arc; 2 3 use egui::{ 4 Area, Color32, CornerRadius, CursorIcon, FontData, FontDefinitions, FontFamily, Frame, Id, 5 Label, Margin, Pos2, Rect, Sense, Shadow, TextWrapMode, Vec2, Visuals, 6 }; 7 use web_sys::js_sys::Math::random; 8 9 const APP_KEY: &str = "poetrymagnets.online"; 10 const FONT_KEY: &str = "Libre Baskerville"; 11 12 const TOP_LEFT: Pos2 = Pos2::ZERO; 13 const BOTTOM_RIGHT: Pos2 = Pos2::new(2048., 2048.); 14 const WORLD_BOUNDARY: Rect = Rect::from_min_max(TOP_LEFT, BOTTOM_RIGHT); 15 16 #[derive(serde::Deserialize, serde::Serialize)] 17 struct Magnet { 18 id: Id, 19 text: String, 20 position: Pos2, 21 } 22 23 #[derive(serde::Deserialize, serde::Serialize)] 24 #[serde(default)] 25 pub struct PoetryMagnetsOnlineApp { 26 zoom_level: f32, 27 zoom_level_target: f32, 28 pan_offset: Pos2, 29 magnets: Vec<Magnet>, 30 } 31 32 impl Default for PoetryMagnetsOnlineApp { 33 fn default() -> Self { 34 let mut magnets = 35 serde_json::from_str::<Vec<Magnet>>(include_str!("../words.json")).unwrap_or_default(); 36 for magnet in magnets.iter_mut() { 37 magnet.position = Pos2::new( 38 random() as f32 * (BOTTOM_RIGHT.x - 50.), 39 random() as f32 * (BOTTOM_RIGHT.y - 25.), 40 ); 41 } 42 Self { 43 zoom_level: 2., 44 zoom_level_target: 2., 45 pan_offset: Pos2::ZERO, 46 magnets, 47 } 48 } 49 } 50 51 impl PoetryMagnetsOnlineApp { 52 pub fn new(cc: &eframe::CreationContext<'_>) -> Self { 53 let mut fonts = FontDefinitions::default(); 54 fonts.font_data.insert( 55 FONT_KEY.to_owned(), 56 Arc::new(FontData::from_static(include_bytes!( 57 "../assets/LibreBaskerville-Regular.ttf" 58 ))), 59 ); 60 fonts 61 .families 62 .get_mut(&FontFamily::Proportional) 63 .unwrap() 64 .insert(0, FONT_KEY.to_owned()); 65 cc.egui_ctx.set_fonts(fonts); 66 cc.egui_ctx.set_visuals(Visuals::light()); 67 68 if let Some(storage) = cc.storage { 69 eframe::get_value(storage, APP_KEY).unwrap_or_default() 70 } else { 71 PoetryMagnetsOnlineApp::default() 72 } 73 } 74 } 75 76 impl eframe::App for PoetryMagnetsOnlineApp { 77 fn save(&mut self, storage: &mut dyn eframe::Storage) { 78 eframe::set_value(storage, APP_KEY, self); 79 } 80 fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 81 egui::CentralPanel::default() 82 .frame(egui::Frame::NONE.fill(Color32::GRAY)) 83 .show(ctx, |ui| { 84 let response = 85 ui.allocate_rect(ui.available_rect_before_wrap(), Sense::click_and_drag()); 86 87 if response.dragged() { 88 let new_pan_offset = self.pan_offset + response.drag_delta(); 89 let new_viewport = ctx.viewport_rect().translate(-new_pan_offset.to_vec2()); 90 if new_viewport.left() < WORLD_BOUNDARY.left() { 91 self.pan_offset.x = 0.; 92 } else if WORLD_BOUNDARY.right() < new_viewport.right() { 93 self.pan_offset.x = new_viewport.width() - WORLD_BOUNDARY.right(); 94 } else { 95 self.pan_offset.x += response.drag_delta().x; 96 } 97 if new_viewport.top() <= WORLD_BOUNDARY.top() { 98 self.pan_offset.y = 0.; 99 } else if WORLD_BOUNDARY.bottom() <= new_viewport.bottom() { 100 self.pan_offset.y = new_viewport.height() - WORLD_BOUNDARY.bottom(); 101 } else { 102 self.pan_offset.y += response.drag_delta().y; 103 } 104 ctx.set_cursor_icon(CursorIcon::Move); 105 } 106 107 let zoom_delta_raw = ui.input(|i| { 108 if let Some(multi_touch) = i.multi_touch() { 109 multi_touch.zoom_delta 110 } else { 111 1. - i.raw_scroll_delta.y * 0.001 112 } 113 }); 114 if zoom_delta_raw != 1. { 115 self.zoom_level_target *= zoom_delta_raw; 116 self.zoom_level_target = self.zoom_level_target.clamp(1., 5.); 117 let zoom_delta_smooth = (self.zoom_level_target - self.zoom_level) * 0.25; 118 self.zoom_level += zoom_delta_smooth; 119 self.zoom_level = self.zoom_level.clamp(1., 5.); 120 } 121 ctx.set_zoom_factor(self.zoom_level); 122 }); 123 for magnet in self.magnets.iter_mut() { 124 let magnet_position_with_offset = self.pan_offset + magnet.position.to_vec2(); 125 let magnet_rect_with_buffer = 126 Rect::from_min_size(magnet_position_with_offset, Vec2::new(50., 50.)); 127 if !ctx.content_rect().intersects(magnet_rect_with_buffer) { 128 continue; 129 } 130 let response = Area::new(magnet.id) 131 .current_pos(magnet_position_with_offset) 132 .constrain(false) 133 .show(ctx, |ui| { 134 Frame::NONE 135 .inner_margin(Margin::symmetric(5, 3)) 136 .shadow(Shadow { 137 offset: [1, 1], 138 blur: 0, 139 spread: 0, 140 color: Color32::GRAY, 141 }) 142 .fill(Color32::WHITE) 143 .corner_radius(CornerRadius::same(1)) 144 .show(ui, |ui| { 145 ui.add( 146 Label::new(magnet.text.clone()) 147 .selectable(false) 148 .wrap_mode(TextWrapMode::Extend), 149 ); 150 }); 151 }) 152 .response; 153 if response.dragged() { 154 let new_magnet_position = magnet.position + response.drag_delta(); 155 let magnet_rect = response.rect.translate(-self.pan_offset.to_vec2()); 156 if magnet_rect.left() <= WORLD_BOUNDARY.left() { 157 magnet.position.x = WORLD_BOUNDARY.left(); 158 } else if WORLD_BOUNDARY.right() <= magnet_rect.right() { 159 magnet.position.x = WORLD_BOUNDARY.width() - magnet_rect.width(); 160 } else { 161 magnet.position.x = new_magnet_position.x; 162 } 163 if magnet_rect.top() <= WORLD_BOUNDARY.top() { 164 magnet.position.y = WORLD_BOUNDARY.top(); 165 } else if WORLD_BOUNDARY.bottom() <= magnet_rect.bottom() { 166 magnet.position.y = WORLD_BOUNDARY.height() - magnet_rect.height(); 167 } else { 168 magnet.position.y = new_magnet_position.y; 169 } 170 ctx.set_cursor_icon(CursorIcon::Grabbing); 171 } else if response.hovered() { 172 ctx.set_cursor_icon(CursorIcon::Grab); 173 } 174 } 175 } 176 }