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:
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);
+ }
}
}
}