poetrymagnets.online

git clone https://git.bradleygannon.com/poetrymagnets.online.git
Log | Files | Refs

commit 4554bd26ffdb948af99145d6172d5568fb61dc0a
parent 99238e5dac05e00db4d67833d4bd5e390c1a6981
Author: Bradley Gannon <bradley@bradleygannon.com>
Date:   Mon, 16 Mar 2026 21:10:47 -0400

Add boundaries, autosave, and better magnet styling

Diffstat:
Afrontend/assets/LibreBaskerville-Regular.ttf | 0
Afrontend/assets/OFL.txt | 93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mfrontend/src/app.rs | 141+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
3 files changed, 196 insertions(+), 38 deletions(-)

diff --git a/frontend/assets/LibreBaskerville-Regular.ttf b/frontend/assets/LibreBaskerville-Regular.ttf Binary files differ. diff --git a/frontend/assets/OFL.txt b/frontend/assets/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2012 The Libre Baskerville Project Authors (https://github.com/impallari/Libre-Baskerville) with Reserved Font Name Libre Baskerville. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/frontend/src/app.rs b/frontend/src/app.rs @@ -1,10 +1,17 @@ +use std::sync::Arc; + use egui::{ - Area, Color32, CornerRadius, Frame, Id, Label, Margin, Pos2, Rect, Sense, TextWrapMode, Vec2, + Area, Color32, CornerRadius, CursorIcon, FontData, FontDefinitions, FontFamily, Frame, Id, + Label, Margin, Pos2, Rect, Sense, Shadow, TextWrapMode, Vec2, Visuals, }; use web_sys::js_sys::Math::random; +const APP_KEY: &str = "poetrymagnets.online"; +const FONT_KEY: &str = "Libre Baskerville"; + const TOP_LEFT: Pos2 = Pos2::ZERO; const BOTTOM_RIGHT: Pos2 = Pos2::new(2048., 2048.); +const WORLD_BOUNDARY: Rect = Rect::from_min_max(TOP_LEFT, BOTTOM_RIGHT); #[derive(serde::Deserialize, serde::Serialize)] struct Magnet { @@ -28,8 +35,8 @@ impl Default for PoetryMagnetsOnlineApp { serde_json::from_str::<Vec<Magnet>>(include_str!("../words.json")).unwrap_or_default(); for magnet in magnets.iter_mut() { magnet.position = Pos2::new( - random() as f32 * BOTTOM_RIGHT.x, - random() as f32 * BOTTOM_RIGHT.y, + random() as f32 * (BOTTOM_RIGHT.x - 50.), + random() as f32 * (BOTTOM_RIGHT.y - 25.), ); } Self { @@ -42,42 +49,77 @@ impl Default for PoetryMagnetsOnlineApp { } impl PoetryMagnetsOnlineApp { - pub fn new(_cc: &eframe::CreationContext<'_>) -> Self { - PoetryMagnetsOnlineApp::default() + pub fn new(cc: &eframe::CreationContext<'_>) -> Self { + let mut fonts = FontDefinitions::default(); + fonts.font_data.insert( + FONT_KEY.to_owned(), + Arc::new(FontData::from_static(include_bytes!( + "../assets/LibreBaskerville-Regular.ttf" + ))), + ); + fonts + .families + .get_mut(&FontFamily::Proportional) + .unwrap() + .insert(0, FONT_KEY.to_owned()); + cc.egui_ctx.set_fonts(fonts); + cc.egui_ctx.set_visuals(Visuals::light()); + + if let Some(storage) = cc.storage { + eframe::get_value(storage, APP_KEY).unwrap_or_default() + } else { + PoetryMagnetsOnlineApp::default() + } } } impl eframe::App for PoetryMagnetsOnlineApp { fn save(&mut self, storage: &mut dyn eframe::Storage) { - eframe::set_value(storage, eframe::APP_KEY, self); + eframe::set_value(storage, APP_KEY, self); } fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - egui::CentralPanel::default().show(ctx, |ui| { - let response = ui.allocate_rect( - ui.available_rect_before_wrap(), - egui::Sense::click_and_drag(), - ); + egui::CentralPanel::default() + .frame(egui::Frame::NONE.fill(Color32::GRAY)) + .show(ctx, |ui| { + let response = + ui.allocate_rect(ui.available_rect_before_wrap(), Sense::click_and_drag()); - if response.dragged() { - self.pan_offset += response.drag_delta(); - } + if response.dragged() { + let new_pan_offset = self.pan_offset + response.drag_delta(); + let new_viewport = ctx.viewport_rect().translate(-new_pan_offset.to_vec2()); + if new_viewport.left() < WORLD_BOUNDARY.left() { + self.pan_offset.x = 0.; + } else if WORLD_BOUNDARY.right() < new_viewport.right() { + self.pan_offset.x = new_viewport.width() - WORLD_BOUNDARY.right(); + } else { + self.pan_offset.x += response.drag_delta().x; + } + if new_viewport.top() <= WORLD_BOUNDARY.top() { + self.pan_offset.y = 0.; + } else if WORLD_BOUNDARY.bottom() <= new_viewport.bottom() { + self.pan_offset.y = new_viewport.height() - WORLD_BOUNDARY.bottom(); + } else { + self.pan_offset.y += response.drag_delta().y; + } + ctx.set_cursor_icon(CursorIcon::Move); + } - let zoom_delta_raw = ui.input(|i| { - if let Some(multi_touch) = i.multi_touch() { - multi_touch.zoom_delta - } else { - 1. - i.raw_scroll_delta.y * 0.001 + let zoom_delta_raw = ui.input(|i| { + if let Some(multi_touch) = i.multi_touch() { + multi_touch.zoom_delta + } else { + 1. - i.raw_scroll_delta.y * 0.001 + } + }); + if zoom_delta_raw != 1. { + self.zoom_level_target *= zoom_delta_raw; + self.zoom_level_target = self.zoom_level_target.clamp(1., 5.); + let zoom_delta_smooth = (self.zoom_level_target - self.zoom_level) * 0.25; + self.zoom_level += zoom_delta_smooth; + self.zoom_level = self.zoom_level.clamp(1., 5.); } + ctx.set_zoom_factor(self.zoom_level); }); - if zoom_delta_raw != 1. { - self.zoom_level_target *= zoom_delta_raw; - self.zoom_level_target = self.zoom_level_target.clamp(1., 5.); - let zoom_delta_smooth = (self.zoom_level_target - self.zoom_level) * 0.25; - self.zoom_level += zoom_delta_smooth; - self.zoom_level = self.zoom_level.clamp(1., 5.); - } - ctx.set_zoom_factor(self.zoom_level); - }); for magnet in self.magnets.iter_mut() { let magnet_position_with_offset = self.pan_offset + magnet.position.to_vec2(); let magnet_rect_with_buffer = @@ -85,27 +127,50 @@ impl eframe::App for PoetryMagnetsOnlineApp { if !ctx.content_rect().intersects(magnet_rect_with_buffer) { continue; } - Area::new(magnet.id) + let response = Area::new(magnet.id) .current_pos(magnet_position_with_offset) .constrain(false) .show(ctx, |ui| { Frame::NONE - .inner_margin(Margin::same(3)) + .inner_margin(Margin::symmetric(5, 3)) + .shadow(Shadow { + offset: [1, 1], + blur: 0, + spread: 0, + color: Color32::GRAY, + }) .fill(Color32::WHITE) .corner_radius(CornerRadius::same(1)) .show(ui, |ui| { - let response = ui.add( + ui.add( Label::new(magnet.text.clone()) .selectable(false) - .wrap_mode(TextWrapMode::Extend) - .sense(Sense::click_and_drag()), + .wrap_mode(TextWrapMode::Extend), ); - if response.dragged() { - magnet.position = (magnet.position + response.drag_delta()) - .clamp(TOP_LEFT, BOTTOM_RIGHT); - } }); - }); + }) + .response; + if response.dragged() { + let new_magnet_position = magnet.position + response.drag_delta(); + let magnet_rect = response.rect.translate(-self.pan_offset.to_vec2()); + if magnet_rect.left() <= WORLD_BOUNDARY.left() { + magnet.position.x = WORLD_BOUNDARY.left(); + } else if WORLD_BOUNDARY.right() <= magnet_rect.right() { + magnet.position.x = WORLD_BOUNDARY.width() - magnet_rect.width(); + } else { + magnet.position.x = new_magnet_position.x; + } + if magnet_rect.top() <= WORLD_BOUNDARY.top() { + magnet.position.y = WORLD_BOUNDARY.top(); + } else if WORLD_BOUNDARY.bottom() <= magnet_rect.bottom() { + magnet.position.y = WORLD_BOUNDARY.height() - magnet_rect.height(); + } else { + magnet.position.y = new_magnet_position.y; + } + ctx.set_cursor_icon(CursorIcon::Grabbing); + } else if response.hovered() { + ctx.set_cursor_icon(CursorIcon::Grab); + } } } }