ui abstraction

This commit is contained in:
Theis Pieter Hollebeek 2025-03-03 19:09:55 +01:00
parent ecbb7c6756
commit 489c2814e2
4 changed files with 250 additions and 150 deletions

View File

@ -1,4 +1,5 @@
mod audio;
mod render;
mod ui;
pub use render::start_game;

View File

@ -1,7 +1,6 @@
use crate::actions::{Action, ActionsHeld};
use crate::board::Board;
use crate::game::{CurrentTetromino, Game};
use crate::Rgb;
use sdl2::event::Event;
use sdl2::keyboard::Keycode;
use sdl2::pixels::Color;
@ -11,146 +10,109 @@ use sdl2::ttf::Sdl2TtfContext;
use std::time::Duration;
use super::audio::{self};
use super::ui::{Rgb, UiCtx};
const WIDTH: i32 = 1000;
const HEIGHT: i32 = 800;
fn draw_board_tile(
canvas: &mut WindowCanvas,
width: usize,
height: usize,
color: &Rgb,
filled: bool,
) -> Result<(), String> {
canvas.set_draw_color(Color::RGB(color.0, color.1, color.2));
let rect = Rect::new(
center(24 * Board::WIDTH as i32, WIDTH) + width as i32 * 24,
center(24 * Board::HEIGHT as i32, HEIGHT) + height as i32 * 24,
24,
24,
);
if filled {
canvas.fill_rect(rect)
} else {
canvas.draw_rect(rect)
}
}
fn center(length: i32, max: i32) -> i32 {
(max - length) / 2
}
fn draw_tetromino_from_parts(
canvas: &mut WindowCanvas,
x: i8,
y: i8,
color: Rgb,
pattern: [[bool; 4]; 4],
filled: bool,
) -> Result<(), String> {
for (y_offset, row) in pattern.iter().enumerate() {
for x_offset in row
.iter()
.enumerate()
.filter(|(_, exists)| **exists)
.map(|(x, _)| x)
{
let x = x_offset as i8 + x;
let y = y_offset as i8 + y;
if y < 0 {
continue;
}
draw_board_tile(canvas, x as usize, y as usize, &color, filled)?
}
}
Ok(())
}
fn draw_board(
canvas: &mut WindowCanvas,
board: &Board,
current: &CurrentTetromino,
) -> Result<(), String> {
canvas.set_draw_color(Color::WHITE);
canvas.draw_rect(Rect::new(
center(24 * Board::WIDTH as i32, WIDTH) - 1,
center(24 * Board::HEIGHT as i32, HEIGHT) - 1,
24 * Board::WIDTH as u32 + 2,
24 * Board::HEIGHT as u32 + 2,
))?;
for (y, row) in board.iter().enumerate() {
for (x, piece) in row.iter().enumerate() {
let color = match piece {
Some(t) => Rgb::from_tetromino(t),
None => Rgb(0, 0, 0),
};
draw_board_tile(canvas, x, y, &color, true)?
}
}
let pattern = current.tetromino.direction_pattern(&current.direction);
draw_tetromino_from_parts(
canvas,
current.x,
board.lowest_y(&current),
Rgb(255, 255, 255),
pattern,
false,
)?;
draw_tetromino_from_parts(
canvas,
current.x,
current.y,
Rgb::from_tetromino(&current.tetromino),
pattern,
true,
)?;
Ok(())
}
fn font_texture<'font, 'a, C>(
text: &'a str,
fn font_texture<'font, 'a, P: AsRef<std::path::Path>, Text: AsRef<str>, C>(
font: P,
text: Text,
ttf_context: &'a Sdl2TtfContext,
texture_creator: &'font TextureCreator<C>,
) -> Result<Texture<'font>, String> {
let font = ttf_context.load_font("resources/josenfin_sans_regular.ttf", 24)?;
let game_over_text = font.render(text).solid(Color::RGB(255, 255, 255)).unwrap();
let font = ttf_context.load_font(font, 24)?;
let game_over_text = font
.render(text.as_ref())
.solid(Color::RGB(255, 255, 255))
.map_err(|err| err.to_string())?;
let texture = texture_creator
.create_texture_from_surface(game_over_text)
.unwrap();
.map_err(|err| err.to_string())?;
Ok(texture)
}
fn draw_important_text(
text: &str,
canvas: &mut WindowCanvas,
ttf_context: &Sdl2TtfContext,
) -> Result<(), String> {
let texture_creator = canvas.texture_creator();
let texture = font_texture(text, &ttf_context, &texture_creator)?;
struct SdlUiCtx {
canvas: WindowCanvas,
ttf: Sdl2TtfContext,
}
let size = texture.query();
let width = size.width;
let height = size.height;
impl SdlUiCtx {
fn present(&mut self) {
self.canvas.present();
}
}
let x = center(width as i32, WIDTH);
let y = center(height as i32, HEIGHT);
impl UiCtx<String> for SdlUiCtx {
fn window_size(&self) -> Result<(i32, i32), String> {
let (width, height) = self.canvas.window().size();
Ok((width as i32, height as i32))
}
canvas.set_draw_color(Color::WHITE);
canvas.draw_rect(Rect::new(x - 9, y - 9, width + 18, height + 18))?;
fn fill_rect(
&mut self,
x: i32,
y: i32,
width: i32,
height: i32,
rgb: &super::ui::Rgb,
) -> Result<(), String> {
self.canvas.set_draw_color(Color::RGB(rgb.0, rgb.1, rgb.2));
self.canvas
.fill_rect(Rect::new(x, y, width as u32, height as u32))?;
Ok(())
}
canvas.set_draw_color(Color::RGB(16, 16, 16));
canvas.fill_rect(Rect::new(x - 8, y - 8, width + 16, height + 16))?;
canvas.copy(&texture, None, Some(Rect::new(x, y, width, height)))?;
fn outline_rect(
&mut self,
x: i32,
y: i32,
width: i32,
height: i32,
rgb: &super::ui::Rgb,
) -> Result<(), String> {
self.canvas.set_draw_color(Color::RGB(rgb.0, rgb.1, rgb.2));
self.canvas
.draw_rect(Rect::new(x, y, width as u32, height as u32))?;
Ok(())
}
Ok(())
fn text_size<P: AsRef<std::path::Path>, Text: AsRef<str>>(
&mut self,
font: P,
text: Text,
) -> Result<(i32, i32), String> {
let texture_creator = self.canvas.texture_creator();
let texture = font_texture(font, text, &self.ttf, &texture_creator)?;
let query = texture.query();
Ok((query.width as i32, query.height as i32))
}
fn fill_text<P: AsRef<std::path::Path>, Text: AsRef<str>>(
&mut self,
font: P,
text: Text,
x: i32,
y: i32,
width: i32,
height: i32,
) -> Result<(), String> {
let texture_creator = self.canvas.texture_creator();
let texture = font_texture(font, text, &self.ttf, &texture_creator)?;
self.canvas.copy(
&texture,
None,
Some(Rect::new(x, y, width as u32, height as u32)),
)?;
Ok(())
}
fn clear(&mut self, rgb: &Rgb) -> Result<(), String> {
self.canvas.set_draw_color(Color::RGB(rgb.0, rgb.1, rgb.2));
self.canvas.clear();
Ok(())
}
}
pub fn start_game() -> Result<(), String> {
@ -170,11 +132,14 @@ pub fn start_game() -> Result<(), String> {
.build()
.unwrap();
let mut canvas = window.into_canvas().build().unwrap();
let canvas = window.into_canvas().build().unwrap();
let mut ctx = SdlUiCtx {
canvas,
ttf: ttf_context,
};
let mut event_pump = sdl_context.event_pump()?;
'running: loop {
canvas.set_draw_color(Color::RGB(16, 16, 16));
canvas.clear();
ctx.clear(&Rgb(16, 16, 16))?;
for event in event_pump.poll_iter() {
match event {
Event::Quit { .. }
@ -231,19 +196,17 @@ pub fn start_game() -> Result<(), String> {
}
}
draw_board(&mut canvas, &game.board, &game.current_tetromino)?;
ctx.draw_board(&game.board, &game.current_tetromino)?;
if paused {
draw_important_text(
ctx.draw_important_text(
"resources/josenfin_sans_regular.ttf",
"game paused o_o... press [p] to unpause !!",
&mut canvas,
&ttf_context,
)?;
} else if game.game_over {
draw_important_text(
ctx.draw_important_text(
"resources/josenfin_sans_regular.ttf",
"game over T_T... press [enter] 2 restart :D",
&mut canvas,
&ttf_context,
)?;
} else {
let effects = game.step(&actions);
@ -254,7 +217,7 @@ pub fn start_game() -> Result<(), String> {
});
}
canvas.present();
ctx.present();
::std::thread::sleep(Duration::new(0, 1_000_000_000u32 / 60));
}
}

152
src/gui/ui.rs Normal file
View File

@ -0,0 +1,152 @@
use crate::{board::Board, game::CurrentTetromino, tetromino::Tetromino};
pub trait UiCtx<Err> {
fn window_size(&self) -> Result<(i32, i32), Err>;
fn fill_rect(&mut self, x: i32, y: i32, width: i32, height: i32, rgb: &Rgb) -> Result<(), Err>;
fn outline_rect(
&mut self,
x: i32,
y: i32,
width: i32,
height: i32,
rgb: &Rgb,
) -> Result<(), Err>;
fn text_size<P: AsRef<std::path::Path>, Text: AsRef<str>>(
&mut self,
font: P,
text: Text,
) -> Result<(i32, i32), Err>;
fn fill_text<P: AsRef<std::path::Path>, Text: AsRef<str>>(
&mut self,
font: P,
text: Text,
x: i32,
y: i32,
width: i32,
height: i32,
) -> Result<(), Err>;
fn clear(&mut self, rgb: &Rgb) -> Result<(), Err>;
fn draw_tetromino_from_parts(
&mut self,
x: i8,
y: i8,
color: Rgb,
pattern: [[bool; 4]; 4],
filled: bool,
) -> Result<(), Err> {
for (y_offset, row) in pattern.iter().enumerate() {
for x_offset in row
.iter()
.enumerate()
.filter(|(_, exists)| **exists)
.map(|(x, _)| x)
{
let x = x_offset as i8 + x;
let y = y_offset as i8 + y;
if y < 0 {
continue;
}
self.draw_board_tile(x as i32, y as i32, &color, filled)?
}
}
Ok(())
}
fn draw_board_tile(&mut self, x: i32, y: i32, color: &Rgb, filled: bool) -> Result<(), Err> {
let tile_size = 24;
let (win_width, win_height) = self.window_size()?;
let x = center(tile_size * Board::WIDTH as i32, win_width) + x * tile_size;
let y = center(tile_size * Board::HEIGHT as i32, win_height) + y * tile_size;
if filled {
self.fill_rect(x, y, 24, 24, color)?;
} else {
self.outline_rect(x, y, 24, 24, color)?;
}
Ok(())
}
fn draw_board(&mut self, board: &Board, current: &CurrentTetromino) -> Result<(), Err> {
let (win_width, win_height) = self.window_size()?;
self.outline_rect(
center(24 * Board::WIDTH as i32, win_width) - 1,
center(24 * Board::HEIGHT as i32, win_height) - 1,
24 * Board::WIDTH as i32 + 2,
24 * Board::HEIGHT as i32 + 2,
&Rgb(255, 255, 255),
)?;
for (y, row) in board.iter().enumerate() {
for (x, piece) in row.iter().enumerate() {
let color = match piece {
Some(t) => Rgb::from_tetromino(t),
None => Rgb(0, 0, 0),
};
self.draw_board_tile(x as i32, y as i32, &color, true)?
}
}
let pattern = current.tetromino.direction_pattern(&current.direction);
self.draw_tetromino_from_parts(
current.x,
board.lowest_y(&current),
Rgb(255, 255, 255),
pattern,
false,
)?;
self.draw_tetromino_from_parts(
current.x,
current.y,
Rgb::from_tetromino(&current.tetromino),
pattern,
true,
)?;
Ok(())
}
fn draw_important_text<P: AsRef<std::path::Path>, Text: AsRef<str>>(
&mut self,
font: P,
text: Text,
) -> Result<(), Err> {
let (win_width, win_height) = self.window_size()?;
let size = self.text_size(font.as_ref(), text.as_ref())?;
let width = size.0;
let height = size.1;
let x = center(width, win_width);
let y = center(height, win_height);
self.outline_rect(x - 9, y - 9, width + 18, height + 18, &Rgb(255, 255, 255))?;
self.fill_rect(x - 8, y - 8, width + 16, height + 16, &Rgb(16, 16, 16))?;
self.fill_text(font, text, x, y, width, height)?;
Ok(())
}
}
pub struct Rgb(pub u8, pub u8, pub u8);
impl Rgb {
pub fn from_tetromino(tetromino: &Tetromino) -> Self {
match tetromino {
Tetromino::I => Self(0, 255, 255),
Tetromino::J => Self(0, 0, 255),
Tetromino::L => Self(255, 128, 0),
Tetromino::O => Self(255, 255, 0),
Tetromino::S => Self(0, 255, 0),
Tetromino::T => Self(255, 0, 255),
Tetromino::Z => Self(255, 0, 0),
}
}
}
fn center(length: i32, max: i32) -> i32 {
(max - length) / 2
}

View File

@ -6,22 +6,6 @@ mod game;
mod gui;
mod tetromino;
struct Rgb(u8, u8, u8);
impl Rgb {
fn from_tetromino(tetromino: &Tetromino) -> Self {
match tetromino {
Tetromino::I => Self(0, 255, 255),
Tetromino::J => Self(0, 0, 255),
Tetromino::L => Self(255, 128, 0),
Tetromino::O => Self(255, 255, 0),
Tetromino::S => Self(0, 255, 0),
Tetromino::T => Self(255, 0, 255),
Tetromino::Z => Self(255, 0, 0),
}
}
}
fn main() {
gui::start_game().unwrap();
}