Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,12 @@ path = "nannou_basics/multi_window_draw.rs"
[[example]]
name = "simple_window"
path = "nannou_basics/simple_window.rs"
[[example]]
name = "loop_once"
path = "nannou_basics/loop_once.rs"
[[example]]
name = "run_modes"
path = "nannou_basics/run_modes.rs"

# Offline
[[example]]
Expand Down
35 changes: 35 additions & 0 deletions examples/nannou_basics/loop_once.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//! `loop_once` - draw a single frame, then hold it on screen.
//!
//! `view` runs exactly once: the composition is drawn and then frozen. The window
//! idles (no CPU) and stays closable and resizable, but nothing ever redraws -
//! waiting or moving the mouse changes nothing. This is the modern equivalent of
//! the old `LoopMode::loop_once()`, ideal for static, sketch-based art.
//!
//! Swap `loop_once()` for `loop_ntimes(n)` to advance `n` frames first, or drop it
//! entirely to animate continuously.

use nannou::prelude::*;

fn main() {
nannou::sketch(view).size(600, 600).loop_once().run();
}

fn view(app: &App) {
let draw = app.draw();
draw.background().color(SNOW);

// A static spiral of circles, drawn once and then held on screen.
let win = app.window_rect();
let n = 240;
for i in 0..n {
let t = i as f32 / n as f32;
let angle = t * PI * 24.0;
let radius = t * win.w() * 0.45;
let x = angle.cos() * radius;
let y = angle.sin() * radius;
draw.ellipse()
.x_y(x, y)
.radius(6.0 * (1.0 - t) + 1.0)
.hsv(t, 0.7, 0.9);
}
}
130 changes: 130 additions & 0 deletions examples/nannou_basics/run_modes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
//! Switch run modes and update modes at runtime to feel how each behaves.
//!
//! Watch the orbiting dot and the `updates` counter - the dot's position follows the
//! counter, so it freezes whenever `update` stops running:
//! - Continuous : counter climbs every frame; the dot orbits smoothly.
//! - Rate (10) : counter climbs ~10x/sec.
//! - Wait : counter climbs only when you give input (move the mouse / press keys).
//! - loop_once : counter stops at 1; the frame is held (~0 CPU).
//! - loop_ntimes(60): counter stops at 60, then held.
//!
//! Keys:
//! Space toggle Continuous <-> loop_once
//! 1 Continuous 2 Rate(10) 3 Wait 4 loop_once 5 loop_ntimes(60)

use nannou::prelude::*;

fn main() {
nannou::app(model).update(update).run();
}

#[derive(Clone, Copy, PartialEq)]
enum Mode {
Continuous,
Rate,
Wait,
LoopOnce,
Loop60,
}

impl Mode {
fn label(self) -> &'static str {
match self {
Mode::Continuous => "Continuous",
Mode::Rate => "Reactive rate (10 fps)",
Mode::Wait => "Wait (redraw on input)",
Mode::LoopOnce => "loop_once (draw once, held)",
Mode::Loop60 => "loop_ntimes(60)",
}
}
}

struct Model {
mode: Mode,
count: u32,
}

fn model(app: &App) -> Model {
app.new_window()
.size(640, 640)
.view(view)
.key_pressed(key_pressed)
.build();
Model {
mode: Mode::Continuous,
count: 0,
}
}

fn update(_app: &App, model: &mut Model) {
model.count += 1;
}

fn key_pressed(app: &App, model: &mut Model, key: KeyCode) {
let mode = match key {
KeyCode::Space => {
if model.mode == Mode::LoopOnce {
Mode::Continuous
} else {
Mode::LoopOnce
}
}
KeyCode::Digit1 => Mode::Continuous,
KeyCode::Digit2 => Mode::Rate,
KeyCode::Digit3 => Mode::Wait,
KeyCode::Digit4 => Mode::LoopOnce,
KeyCode::Digit5 => Mode::Loop60,
_ => return,
};
set_mode(app, model, mode);
}

// Apply a mode as a (run mode, update mode) pair. Loop modes only set the run mode - the
// framework drives them and then freezes; the others are `UntilExit` with an explicit
// update mode.
fn set_mode(app: &App, model: &mut Model, mode: Mode) {
model.mode = mode;
model.count = 0;
match mode {
Mode::Continuous => {
app.set_run_mode(RunMode::UntilExit);
app.set_update_mode(UpdateMode::Continuous);
}
Mode::Rate => {
app.set_run_mode(RunMode::UntilExit);
app.set_update_rate(10.0);
}
Mode::Wait => {
app.set_run_mode(RunMode::UntilExit);
app.set_update_mode(UpdateMode::wait());
}
Mode::LoopOnce => app.set_run_mode(RunMode::loop_once()),
Mode::Loop60 => app.set_run_mode(RunMode::loop_ntimes(60)),
}
}

fn view(app: &App, model: &Model) {
let draw = app.draw();
draw.background().srgb(0.09, 0.10, 0.13);
let win = app.window_rect();

// Orbiting dot - position advances with the update counter.
let a = model.count as f32 * 0.08;
let r = win.w().min(win.h()) * 0.3;
draw.ellipse()
.x_y(a.cos() * r, a.sin() * r)
.radius(24.0)
.color(TOMATO);

// HUD: current mode + update count, and the key hints.
draw.text(&format!("{}\nupdates: {}", model.mode.label(), model.count))
.x_y(0.0, win.h() * 0.5 - 44.0)
.wh(vec2(win.w() - 20.0, 80.0))
.font_size(22)
.color(WHITE);
draw.text("space: Continuous <-> loop_once 1 Continuous 2 Rate 3 Wait 4 loop_once 5 loop60")
.x_y(0.0, -win.h() * 0.5 + 24.0)
.wh(vec2(win.w() - 20.0, 30.0))
.font_size(13)
.color(GRAY);
}
38 changes: 38 additions & 0 deletions guide/src/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,44 @@ back to the origins.

---

# Unreleased

- Added `RunMode::loop_once()` and `RunMode::loop_ntimes(n)`, with matching
`.loop_once()` / `.loop_ntimes(n)` shortcuts on both the app `Builder` and the
`SketchBuilder`. These run `update` and `view` a fixed number of times and then
hold the last frame on screen while the window idles (~0 CPU) and stays closable
and resizable - the modern equivalent of the old `LoopMode::loop_once()` /
`NTimes`, ideal for static `sketch`-based compositions. This fills a gap left by
the Bevy refactor: no single `UpdateMode` could both ignore mouse movement and
stay responsive, because `bevy_winit` cannot distinguish cursor movement from
window close/resize (both are "window events"). The static frame is held by
freezing the draw (the rendered meshes persist and keep being drawn) rather than
by re-running `view`, so `view` runs exactly `n` times even for a sketch that
reads live time or input.

Migrating from the pre-0.20 `LoopMode`:

- `LoopMode::loop_once()` -> `RunMode::loop_once()` (or `.loop_once()` on the app
or sketch builder).
- `LoopMode::loop_ntimes(n)` -> `RunMode::loop_ntimes(n)`.
- `LoopMode::Wait` -> `app.set_update_mode(UpdateMode::wait())`.
- `LoopMode::Rate` / `rate_fps(fps)` -> `app.set_update_rate(fps)` or
`UpdateMode::rate(hz)`.
- `LoopMode::RefreshSync` -> `UpdateMode::Continuous` (the default).

- Added `App::set_run_mode(..)` to change the `RunMode` at runtime (e.g. to switch
into or out of a loop mode), mirroring `App::set_update_mode`. Entering a loop mode
resets its budget and drives the loop until it freezes, so loop modes can be entered
and re-entered live. See the new `run_modes` example for switching between
Continuous, reactive-rate, wait, and loop modes with the keyboard.

- Removed `RunMode::Ticks(n)`, `RunMode::Duration(..)` and `RunMode::once()`. They
were unused, undocumented, and `once()` (quit after one frame) was easily confused
with `loop_once()` (hold after one frame). To run a fixed number of frames and then
quit, call `App::quit()` from your own `update` once a counter reaches the target.

---

# Version 0.20.0 (2026-06-20)

## The Bevy Refactor
Expand Down
Loading
Loading