editor-gui: a duration palette over set_selection_duration

A toolbar palette — 1 / 1/2 / 1/4 / 1/8 / 1/16 — sets the selected note or
rest's written value via set_selection_duration(NoteValue::whole_note_fraction),
with the editor's make-room overwrite when lengthening; refusals (tuplet member,
decomposed event, non-note selection) surface in the status line. The palette
lives in the toolbar, which runs before the frame's rerender slot, so it has no
stale-texture concern. Adds epiphany-core as a direct dependency for NoteValue
(already a transitive workspace member, so no MSRV/CI impact). The module doc and
crate description are updated; only an undo UI remains unbuilt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NAtEiJtt9yKVV1zjKYmZhS
This commit is contained in:
Levi Neuwirth 2026-06-30 16:32:30 -04:00
parent 0f298508af
commit 872ff03dd5
3 changed files with 29 additions and 6 deletions

1
Cargo.lock generated
View File

@ -1098,6 +1098,7 @@ name = "epiphany-editor-gui"
version = "0.0.0"
dependencies = [
"eframe",
"epiphany-core",
"epiphany-editor-core",
"epiphany-engrave",
"epiphany-layout-ir",

View File

@ -4,7 +4,7 @@ version = "0.0.0"
edition.workspace = true
authors.workspace = true
repository.workspace = true
description = "A thin native (egui) GUI shell over epiphany-editor-core: renders the score with epiphany-render-svg (rasterized by resvg), click to select, and a toolbar/keys for the note-editing intents. A vertical slice proving the editor seam — no duration editing, empty-space insert, or undo UI yet."
description = "A thin native (egui) GUI shell over epiphany-editor-core: renders the score with epiphany-render-svg (rasterized by resvg), click to select, a pencil mode that clicks notes onto the staff, and a toolbar/keys for the note-editing intents and a duration palette. A vertical slice proving the editor seam — no undo UI yet."
# A demo binary, not a library other crates build on, so it carries the heavy GUI
# dependency tree (eframe/winit/wgpu, resvg/usvg/tiny-skia) the rest of the
@ -14,6 +14,8 @@ description = "A thin native (egui) GUI shell over epiphany-editor-core: renders
[dependencies]
# The headless editor this GUI drives.
epiphany-editor-core.workspace = true
# Musical-duration / note-value types, for the duration palette.
epiphany-core.workspace = true
# The real constraint solver (so the GUI shows a genuinely engraved score).
epiphany-engrave.workspace = true
# SVG rendering of the resolved layout, and the layout/hit-test geometry types.

View File

@ -7,11 +7,13 @@
//! `resvg` into an `egui` texture, resolves clicks back to world coordinates to
//! select, and drives the note-editing intents from a toolbar and keyboard.
//!
//! Intentionally narrow (per the GUI vertical-slice plan): no duration editing and no
//! undo UI — the debug panel just shows the selection and the last applied op, enough
//! to confirm the op log is usable. A **pencil mode** turns a click into a
//! click-to-insert ([`EditorSession::insert_note_at`]) with make-room overwrite, on a
//! quarter-note grid. The GUI is the thing meant to surface the next real core gaps.
//! Intentionally narrow (per the GUI vertical-slice plan): no undo UI — the debug
//! panel just shows the selection and the last applied op, enough to confirm the op log
//! is usable. A **pencil mode** turns a click into a click-to-insert
//! ([`EditorSession::insert_note_at`]) with make-room overwrite on a quarter-note grid,
//! and a **duration palette** resizes the selection
//! ([`EditorSession::set_selection_duration`]). The GUI is the thing meant to surface
//! the next real core gaps.
//!
//! It is a demo binary; there is no headless way to assert its rendering here, so the
//! one piece of nontrivial logic — the screen↔world coordinate map a click depends on
@ -19,6 +21,7 @@
use eframe::egui;
use epiphany_core::NoteValue;
use epiphany_editor_core::{EditOutcome, EditorError, EditorSession, GridResolution};
use epiphany_engrave::Engraver;
use epiphany_layout_ir::{BoundingBox, HitShape, Point};
@ -247,6 +250,23 @@ impl EditorApp {
self.run("insert after", |s| s.insert_note_after_selection());
}
ui.separator();
// Duration palette: set the selected note/rest's written value (make-room
// overwrite when lengthening).
ui.label("Dur:");
for (label, value) in [
("1", NoteValue::Whole),
("1/2", NoteValue::Half),
("1/4", NoteValue::Quarter),
("1/8", NoteValue::Eighth),
("1/16", NoteValue::Sixteenth),
] {
if ui.button(label).clicked() {
self.run(&format!("duration {label}"), |s| {
s.set_selection_duration(value.whole_note_fraction())
});
}
}
ui.separator();
ui.toggle_value(&mut self.pencil, "✏ Pencil (insert)");
ui.separator();
if ui