Skip to content

Commit

Permalink
more work on adapter, especially adopting it in tuig
Browse files Browse the repository at this point in the history
  • Loading branch information
nic-hartley committed Nov 10, 2023
1 parent ec4b85c commit 5febb24
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 104 deletions.
75 changes: 64 additions & 11 deletions tuig-ui/src/adapter.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,58 @@
use std::time::{Duration, Instant};

use tuig_iosys::{Action, IoSystem, Result, Screen, XY};

use crate::{attachments::Attachment, Region};

/// A convenience wrapper to make it easier to manage screens, actions, and input.
/// A convenience wrapper to make it easier to manage screens, actions, and input in simple cases.
///
/// This is a very simple wrapper. You put in your desired `IoSystem`, and then you can call [`Self::input`],
/// [`Self::poll_input`], and [`Self::draw`] to run things. Internally it stores a [`Screen`], and it manages that
/// in a simple, allocation-avoiding/reusing way.
///
/// This is mean to handle average use-cases. You might well need to do something more specific -- and hopefully, this
/// at least serves as a jumping-off point.
/// This is meant to handle simple, average use-cases. You might well need to do something more specific -- and
/// hopefully, this code is simple enough to serve as a jumping-off point. It includes functionality for:
///
/// - Only rendering if the screen has changed
/// - Capping framerates, optionally
///
/// The biggest benefit is that this API will stay far more stable than the "lower level" ones, even during this early
/// alpha phase, incorporating lots of [planned] [features] more or less seamlessly. The biggest drawback is that it
/// you can't precisely control when or how it does each step, which might be a dealbreaker for advanced use-cases.
/// But the code also tries to be easy enough to read that you can make your own.
pub struct Adapter<IO: IoSystem> {
io: IO,
screen: Screen,
old: Screen,
current: Screen,
fps: Option<(Duration, Instant)>,
}

impl<IO: IoSystem> Adapter<IO> {
/// Create a new [`Adapter`] that will be adapting inputs and outputs from this [`IoSystem`].
/// Create a new `Adapter` that will be adapting inputs and outputs from this [`IoSystem`], with no FPS cap.
pub fn new(io: IO) -> Self {
Self {
io,
screen: Screen::new(XY(0, 0)),
old: Screen::new(XY(0, 0)),
current: Screen::new(XY(0, 0)),
fps: None,
}
}

/// Set an FPS cap on this `Adapter`.
///
/// Remember this just skips calls to [`Self::draw`] when they're too fast, so your framerate will probably be a
/// bit lower than this. If you need to minimize frame times, you'll need to implement more complex logic around
/// calling `draw` yourself.
///
/// Pass 0 to disable a previously set cap. By default, there isn't one.
pub fn with_cap(mut self, max_fps: usize) -> Self {
self.fps = match max_fps {
0 => None,
nz => Some((Duration::from_secs_f32(1.0 / nz as f32), Instant::now())),
};
self
}

/// Wait for one input like [`IoSystem::input`], then use it to render the attachment to the stored screen.
///
/// This returns an error if the `IoSystem` did, or otherwise whatever the root attachment does.
Expand All @@ -42,13 +71,19 @@ impl<IO: IoSystem> Adapter<IO> {
.map(|o| o.map(|input| self.feed(root, input)))
}

/// Rerender the screen by passing an [`Action::Redraw`] into it.
pub fn refresh<'s, A: Attachment<'s>>(&'s mut self, root: A) -> A::Output {
self.feed(root, Action::Redraw)
}

/// Manually feed in an action, doing everything as though it was taken from the `IoSystem`.
///
/// This is probably most obviously useful for `adapter.feed(&mut attachment, Action::Redraw)` shortly before
/// calling [`Self::draw`].
/// This is probably most obviously useful for `adapter.feed(&mut attachment, Action::Redraw)`. But that shouldn't
/// be necessary if you're handling inputs immediately before drawing. Window resizes, etc. will send an
/// [`Action::Redraw`] that'll trigger a rerender anyway.
pub fn feed<'s, A: Attachment<'s>>(&'s mut self, root: A, input: Action) -> A::Output {
self.screen.resize(self.io.size());
let region = Region::new(&mut self.screen, input);
self.current.resize(self.io.size());
let region = Region::new(&mut self.current, input);
region.attach(root)
}

Expand All @@ -57,6 +92,24 @@ impl<IO: IoSystem> Adapter<IO> {
/// This will **not** re-render anything if the screen isn't the right size -- it'll just try to draw. See the
/// caveats in [`IoSystem::draw`] for more info.
pub fn draw(&mut self) -> Result<()> {
self.io.draw(&self.screen)
if self.current == self.old {
return Ok(());
}
if let Some((delta, ref mut next_draw)) = self.fps {
let now = Instant::now();
if now < *next_draw {
return Ok(());
}
*next_draw = now + delta;
}
self.io.draw(&self.current)?;
// preserve the screen we just drew as the old one, start rendering to the old old one
std::mem::swap(&mut self.old, &mut self.current);
Ok(())
}

/// [Stop](IoSystem::stop) the `IoSystem`.
pub fn stop(&mut self) {
self.io.stop()
}
}
19 changes: 4 additions & 15 deletions tuig/src/bin/mass-messages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use std::time::Instant;

use tuig::{Agent, ControlFlow, Game, Replies, Response, Runner};
use tuig::{Agent, ControlFlow, Game, Replies, Runner};
use tuig_ui::Region;

const AGENTS: u64 = 10_000;
Expand Down Expand Up @@ -43,34 +43,23 @@ struct TinyGame {

impl Game for TinyGame {
type Message = TinyMessage;
fn message(&mut self, msg: &Self::Message) -> Response {
fn message(&mut self, msg: &Self::Message) {
if msg != &0 {
self.count += 1;
}
if *msg == 1 {
self.complete += 1;
if self.complete == AGENTS {
Response::Quit
} else {
Response::Redraw
}
} else if *msg > self.max {
self.max = *msg;
Response::Redraw
} else {
if self.count % (AGENTS / 100) == 0 {
Response::Redraw
} else {
Response::Nothing
}
}
}

fn attach<'s>(&mut self, _into: Region<'s>, _replies: &mut Replies<Self::Message>) {
fn attach<'s>(&mut self, _into: Region<'s>, _replies: &mut Replies<Self::Message>) -> bool {
println!(
"count={}, max={}, complete={}",
self.count, self.max, self.complete
);
self.complete == AGENTS
}
}

Expand Down
24 changes: 10 additions & 14 deletions tuig/src/game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,6 @@ use tuig_ui::Region;

use crate::{Message, Replies};

/// How a `Game` can respond to inputs or messages, affecting the whole game.
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum Response {
/// Nothing in particular needs to be done.
Nothing,
/// The visual state has updated, and the [`Screen`](tuig_iosys::Screen) needs to be redrawn.
Redraw,
/// The game should be exited, e.g. because the user clicked "Exit" in the menu.
Quit,
}

/// Represents a game which can be run in the main loop.
///
/// Note that `Game`s don't run the bulk of the game logic; that's the `Agent`'s job. The `Game` trait is the place
Expand All @@ -31,12 +20,19 @@ pub trait Game: Send {
type Message: Message;

/// A message has happened; update the UI accordingly.
fn message(&mut self, message: &Self::Message) -> Response;
fn message(&mut self, message: &Self::Message);

/// Attach the game to a [`Region`] occupying the whole screen. Based on the inputs given, re-render the player's
/// UI, and inform [`Agent`](crate::Agent)s accordingly.
/// UI, and inform [`Agent`](crate::Agent)s accordingly. This always gets called at least once per frame, either
/// when user input happens or with `Redraw` if there was no input during the frame.
///
/// If you want to render in terms of a raw [`Screen`](tuig_iosys::Screen) and input [`Action`](tuig_iosys::Action)
/// instead, call [`Region::attach`] with a [`RawAttachment`](tuig_ui::attachments::RawAttachment).
fn attach(&mut self, into: Region<'_>, replies: &mut Replies<Self::Message>);
///
/// This will blindly pass inputs through to you -- be sure to check for `Closed` events, perform the cleanup you
/// need to do, and return `true` as appropriate. (If cleanup might take a while, e.g. saving the game, consider
/// spawning an agent to do it.)
///
/// Return `true` to completely exit the game, e.g. if the player pressed a "Quit" button in the menu.
fn attach(&mut self, into: Region<'_>, replies: &mut Replies<Self::Message>) -> bool;
}
2 changes: 1 addition & 1 deletion tuig/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ mod util;

pub use {
agent::{Agent, ControlFlow, WaitHandle},
game::{Game, Response},
game::Game,
message::{Message, Replies},
runner::Runner,
tuig_iosys as io,
Expand Down
Loading

0 comments on commit 5febb24

Please sign in to comment.