~saiko/game

9b72bf169aaf3e7f6f006d1755330c711d51a1b9 — 2xsaiko 7 months ago 247a0c3
command input and configurable UI colors
M data/cfg/sys/init.cfg => data/cfg/sys/init.cfg +1 -0
@@ 1,4 1,5 @@
// Set sensible engine defaults.

exec sys/fonts
exec sys/uicol
exec autoexec
\ No newline at end of file

A data/cfg/sys/uicol.cfg => data/cfg/sys/uicol.cfg +10 -0
@@ 0,0 1,10 @@
// Default UI colors. Specified as R, G, B components from 0-255.

ui_col_border     0 130 170
ui_col_normal   102 102 102
ui_col_hilight  255 255 255
ui_col_act      130   0 170
ui_col_disabled  70  70  70
ui_col_query     70  70  70
ui_col_text     255 255 255
ui_col_tsel      17  17  24
\ No newline at end of file

M src/main.rs => src/main.rs +85 -7
@@ 1,11 1,11 @@
#![feature(clamp, slice_patterns, box_syntax, const_fn, drain_filter)]
#![feature(clamp, slice_patterns, box_syntax, const_fn, drain_filter, cell_update)]
#![allow(unused)]
#![warn(unused_imports)]

#[macro_use]
extern crate glium;

use std::cell::RefCell;
use std::cell::{Cell, RefCell};
use std::convert::TryInto;
use std::fs::{create_dir_all, File};
use std::io::Read;


@@ 48,7 48,8 @@ use crate::shader::ShaderManager;
use crate::testui::TestUiController;
use crate::ui::graphics::{DrawPreview, Graphics, StringDrawProps};
use crate::ui::host::UiHost;
use crate::util::{AnySurface, Color, LogPipe};
use crate::ui::settings::UiSettings;
use crate::util::{AnySurface, apply, Color, LogPipe};

mod cam;
mod cmd;


@@ 143,6 144,8 @@ fn start_game(env: Environment) -> ! {

  let running = Arc::new(AtomicBool::new(true));

  let ui_settings = Rc::new(Cell::new(UiSettings::default()));

  let mut ce = {
    let env = env.clone();
    let running = running.clone();


@@ 156,6 159,78 @@ fn start_game(env: Environment) -> ! {
      .with_cvar("fps_limit", ConVar::of_int(120, Some(1), None))
      .with_cvar("debug_info", ConVar::of_bool(false))
      .with_help_text("debug_info", "whether to show debug info like fps and player position")
      .with_command("ui_col_border", {
        let ui_settings = ui_settings.clone();
        box command(move |args, _, _| {
          let r = args.get(0).unwrap_or(&"").parse().unwrap_or(0);
          let g = args.get(1).unwrap_or(&"").parse().unwrap_or(0);
          let b = args.get(2).unwrap_or(&"").parse().unwrap_or(0);
          ui_settings.update(|s| apply(s, |s| s.border_color = Color::from_rgb8(r, g, b)));
        })
      })
      .with_command("ui_col_normal", {
        let ui_settings = ui_settings.clone();
        box command(move |args, _, _| {
          let r = args.get(0).unwrap_or(&"").parse().unwrap_or(0);
          let g = args.get(1).unwrap_or(&"").parse().unwrap_or(0);
          let b = args.get(2).unwrap_or(&"").parse().unwrap_or(0);
          ui_settings.update(|s| apply(s, |s| s.normal_color = Color::from_rgb8(r, g, b)));
        })
      })
      .with_command("ui_col_hilight", {
        let ui_settings = ui_settings.clone();
        box command(move |args, _, _| {
          let r = args.get(0).unwrap_or(&"").parse().unwrap_or(0);
          let g = args.get(1).unwrap_or(&"").parse().unwrap_or(0);
          let b = args.get(2).unwrap_or(&"").parse().unwrap_or(0);
          ui_settings.update(|s| apply(s, |s| s.highlighted_color = Color::from_rgb8(r, g, b)));
        })
      })
      .with_command("ui_col_act", {
        let ui_settings = ui_settings.clone();
        box command(move |args, _, _| {
          let r = args.get(0).unwrap_or(&"").parse().unwrap_or(0);
          let g = args.get(1).unwrap_or(&"").parse().unwrap_or(0);
          let b = args.get(2).unwrap_or(&"").parse().unwrap_or(0);
          ui_settings.update(|s| apply(s, |s| s.active_color = Color::from_rgb8(r, g, b)));
        })
      })
      .with_command("ui_col_disabled", {
        let ui_settings = ui_settings.clone();
        box command(move |args, _, _| {
          let r = args.get(0).unwrap_or(&"").parse().unwrap_or(0);
          let g = args.get(1).unwrap_or(&"").parse().unwrap_or(0);
          let b = args.get(2).unwrap_or(&"").parse().unwrap_or(0);
          ui_settings.update(|s| apply(s, |s| s.disabled_color = Color::from_rgb8(r, g, b)));
        })
      })
      .with_command("ui_col_query", {
        let ui_settings = ui_settings.clone();
        box command(move |args, _, _| {
          let r = args.get(0).unwrap_or(&"").parse().unwrap_or(0);
          let g = args.get(1).unwrap_or(&"").parse().unwrap_or(0);
          let b = args.get(2).unwrap_or(&"").parse().unwrap_or(0);
          ui_settings.update(|s| apply(s, |s| s.query_text_color = Color::from_rgb8(r, g, b)));
        })
      })
      .with_command("ui_col_text", {
        let ui_settings = ui_settings.clone();
        box command(move |args, _, _| {
          let r = args.get(0).unwrap_or(&"").parse().unwrap_or(0);
          let g = args.get(1).unwrap_or(&"").parse().unwrap_or(0);
          let b = args.get(2).unwrap_or(&"").parse().unwrap_or(0);
          ui_settings.update(|s| apply(s, |s| s.text_color = Color::from_rgb8(r, g, b)));
        })
      })
      .with_command("ui_col_tsel", {
        let ui_settings = ui_settings.clone();
        box command(move |args, _, _| {
          let r = args.get(0).unwrap_or(&"").parse().unwrap_or(0);
          let g = args.get(1).unwrap_or(&"").parse().unwrap_or(0);
          let b = args.get(2).unwrap_or(&"").parse().unwrap_or(0);
          ui_settings.update(|s| apply(s, |s| s.text_selected_background_color = Color::from_rgb8(r, g, b)));
        })
      })
      .with_command("exec", box expand_command(move |args, _, out| {
        let file = format!("cfg/{}.cfg", *args.get(0)?);



@@ 211,9 286,10 @@ fn start_game(env: Environment) -> ! {
    fps: FpsCounter::new(),
    camera,
    fr,
    ui_settings,
  };

  game.ui_host.open_ui(TestUiController::new(game.camera.clone(), model.clone()));
  game.ui_host.open_ui(TestUiController::new(game.camera.clone(), model.clone(), game.ce.scheduler().clone()));

  event_loop.run_return(|event, target, cf| game.handle_event(event, target, cf));
  exit(0);


@@ 236,9 312,10 @@ pub struct Game<'a> {
  fps: FpsCounter,
  camera: Rc<RefCell<Camera>>,
  fr: Multifont<(i32, bool, f64), (i32, bool), BakedFont<'a>, SelImpl>,
  ui_settings: Rc<Cell<UiSettings>>,
}

const FONT_SIZE: i32 = 32;
const FONT_SIZE: i32 = 16;

impl Game<'_> {
  fn handle_event(&mut self, event: Event<()>, target: &EventLoopWindowTarget<()>, cf: &mut ControlFlow) {


@@ 333,7 410,8 @@ impl Game<'_> {
      camera,
      display,
      fps,
      fr
      fr,
      ui_settings,
    } = self;

    let display = *display;


@@ 346,7 424,7 @@ impl Game<'_> {
    let gui_mat = Mat4::get_ortho(0.0, window.width() as f32, window.height() as f32, 0.0, -1000.0, 1000.0);

    let mut surface = AnySurface::Frame(&mut frame);
    let mut g = Graphics::new(display, &mut surface, self.fr.choose((FONT_SIZE, false, window.dpi)), &mut self.sm, gui_mat);
    let mut g = Graphics::new(display, &mut surface, self.fr.choose((FONT_SIZE, false, window.dpi)), &mut self.sm, gui_mat, ui_settings.get());

    self.ui_host.draw(&mut g);


M src/testui.rs => src/testui.rs +16 -3
@@ 3,12 3,14 @@ use std::rc::Rc;

use crate::{Model, Vertex};
use crate::cam::Camera;
use crate::cmd::{CommandDispatcher, CommandScheduler, ExecSource, ExecState, Executor};
use crate::ui::controller::UiController;
use crate::ui::element::{Button, InputField, Viewport};
use crate::ui::element::{Button, InputField, InputFieldEvent, Viewport};
use crate::ui::graphics::{DrawPreview, Graphics, StringDrawProps};
use crate::ui::layout::{BorderLayout, LayoutManager};
use crate::ui::panel::Container;
use crate::ui::rescap::ResizeCapabilities;
use crate::util::{apply, LogPipe};

ui_def! {
  pub impl TestUi, TestUiAccess for TestUiElements, TestUiController {


@@ 32,14 34,16 @@ pub struct TestUiController {
  click_counter: u32,
  camera: Rc<RefCell<Camera>>,
  model: Rc<Model<Vertex>>,
  scheduler: CommandScheduler,
}

impl TestUiController {
  pub fn new(camera: Rc<RefCell<Camera>>, model: Rc<Model<Vertex>>) -> TestUi {
  pub fn new(camera: Rc<RefCell<Camera>>, model: Rc<Model<Vertex>>, scheduler: CommandScheduler) -> TestUi {
    TestUi::new(TestUiController {
      click_counter: 0,
      camera,
      model,
      scheduler,
    })
  }



@@ 61,7 65,7 @@ impl UiController for TestUiController {
  fn create_elements(&self) -> Self::Elements {
    TestUiElements {
      button: Button::new("top"),
      button1: InputField::new(),
      button1: apply(InputField::new(), |field| field.set_overlay_text("Enter commands...")),
      button2: Button::new("left"),
      button3: Button::new("right"),
      button4: Viewport::new(self.camera.clone(), self.model.clone()),


@@ 88,6 92,15 @@ impl UiController for TestUiController {
      button2.set_enabled(true);
      button.set_enabled(true);
    });
    button1.poll_events(|b, e| {
      match e {
        InputFieldEvent::EnterPressed => {
          self.scheduler.exec(b.text(), ExecSource::Console);
          b.set_text("");
        },
        _ => {}
      }
    });
  }

  fn resize(&mut self, elements: &mut Self::Elements, g: &Graphics, width: u32, height: u32) {

M src/ui/element/button.rs => src/ui/element/button.rs +9 -8
@@ 1,6 1,6 @@
use glium::glutin::event::{ElementState, ModifiersState, MouseButton};

use crate::ui::element::{ACTIVE_COLOR, default_try_focus, Element, FocusResult, HIGHLIGHTED_COLOR, INACTIVE_COLOR, NORMAL_COLOR};
use crate::ui::element::{default_try_focus, Element, FocusResult};
use crate::ui::event::EventQueue;
use crate::ui::graphics::{Alignment, DrawPreview, Graphics, StringDrawProps};
use crate::ui::panel::Container;


@@ 53,16 53,17 @@ impl Button {

impl Element for Button {
  fn draw(&mut self, g: &mut Graphics) {
    let color = match (self.is_enabled, self.is_mouse_over, self.is_pressed) {
      (false, _, _) => INACTIVE_COLOR,
      (true, false, false) => NORMAL_COLOR,
      (true, _, true) => ACTIVE_COLOR,
      (true, true, false) => HIGHLIGHTED_COLOR,
    let border = if self.is_enabled { g.settings().border_color } else { g.settings().disabled_color };
    let text = match (self.is_enabled, self.is_mouse_over, self.is_pressed) {
      (false, _, _) => g.settings().disabled_color,
      (true, false, false) => g.settings().normal_color,
      (true, _, true) => g.settings().active_color,
      (true, true, false) => g.settings().highlighted_color,
    };

    g.draw_rect(1, 1, self.width as i32 - 2, self.height as i32 - 2, color);
    g.draw_rect(1, 1, self.width as i32 - 2, self.height as i32 - 2, border);
    g.draw_string(&self.text, self.width as i32 / 2, self.height as i32 / 2 - (g.font_renderer().get_height() / 2.0) as i32,
                  StringDrawProps::default().with_alignment(Alignment::Center).with_color(Color::from_packed_rgb(0x7f7f7f)));
                  StringDrawProps::default().with_alignment(Alignment::Center).with_color(text));
  }

  fn set_size(&mut self, width: u32, height: u32) {

M src/ui/element/inputfield.rs => src/ui/element/inputfield.rs +27 -12
@@ 5,13 5,12 @@ use clipboard::{ClipboardContext, ClipboardProvider};
use glium::glutin::event::{ElementState, ModifiersState, MouseButton, ScanCode, VirtualKeyCode};
use unicode_segmentation::UnicodeSegmentation;

use crate::ui::element::{default_try_focus, Element, FocusResult, HIGHLIGHTED_COLOR, INACTIVE_COLOR, NORMAL_COLOR};
use crate::ui::element::{default_try_focus, Element, FocusResult};
use crate::ui::event::EventQueue;
use crate::ui::graphics::{DrawPreview, Graphics, StringDrawProps};
use crate::ui::panel::Container;
use crate::ui::rescap::ResizeCapabilities;
use crate::ui::util::is_in_bounds;
use crate::util::Color;

pub enum InputFieldEvent {
  KeyInput(char),


@@ 53,7 52,11 @@ impl InputField {

  pub fn overlay_text(&self) -> &str { &self.overlay_text }

  pub fn set_text(&mut self, text: &str) { self.text = text.to_owned(); }
  pub fn set_text(&mut self, text: &str) {
    self.text = text.to_owned();
    self.cursor_position = 0;
    self.selection_start = 0;
  }

  pub fn text(&self) -> &str { &self.text }



@@ 61,6 64,12 @@ impl InputField {
    self.text.push(c);
  }

  pub fn poll_events(&mut self, mut op: impl FnMut(&mut Self, InputFieldEvent)) {
    for ev in self.queue.pop_events() {
      op(self, ev);
    }
  }

  fn can_type(&self) -> bool { self.is_enabled && self.is_focused }

  fn search_char_left(&self) -> usize {


@@ 179,10 188,10 @@ impl InputField {

impl Element for InputField {
  fn draw(&mut self, g: &mut Graphics) {
    let color = match (self.is_enabled, self.is_focused) {
      (false, _) => INACTIVE_COLOR,
      (true, false) => NORMAL_COLOR,
      (true, true) => HIGHLIGHTED_COLOR,
    let border = if self.is_enabled { g.settings().border_color } else { g.settings().disabled_color };
    let text = match self.is_enabled {
      false => g.settings().disabled_color,
      true => g.settings().text_color,
    };

    if self.selection_start != self.cursor_position {


@@ 194,18 203,21 @@ impl Element for InputField {

        g.translatef(0.0, 0.0, -0.1);
        // g.draw_rect(x, 3, width, g.font_renderer().get_height() as i32, Color::from_rgb8(0x1a, 0xff, 0x17));
        g.fill_rect(x, 3, width, 1 + g.font_renderer().get_height() as i32, Color::from_rgb8(0x11, 0x11, 0x18));
        g.fill_rect(x, 3, width, 1 + g.font_renderer().get_height() as i32, g.settings().text_selected_background_color);
      });
    }

    g.draw_rect(1, 1, self.width as i32 - 2, self.height as i32 - 2, color);
    g.draw_string(&self.text, 3, 3,
                  StringDrawProps::default().with_color(Color::white()));
    g.draw_rect(1, 1, self.width as i32 - 2, self.height as i32 - 2, border);
    g.draw_string(&self.text, 3, 3, StringDrawProps::default().with_color(text));

    if !self.is_focused && self.text.is_empty() && self.is_enabled {
      g.draw_string(&self.overlay_text, 3, 3, StringDrawProps::default().with_color(g.settings().query_text_color));
    }

    if self.is_focused {
      let width = g.font_renderer().get_height() as i32 / 5;
      let cur_at = 3 + max(width, g.font_renderer().get_str_width(&self.text[..self.cursor_position]) as i32);
      let c = Color::white();
      let c = g.settings().active_color;
      let end = 3 + g.font_renderer().get_height() as i32;
      g.draw_line(cur_at, 4, cur_at, end - 1, c);
      g.draw_line(cur_at - width, 3, cur_at - 1, 3, c);


@@ 256,6 268,9 @@ impl Element for InputField {
    }
    if self.can_type() && state == ElementState::Pressed {
      match vk {
        Some(VirtualKeyCode::Return) | Some(VirtualKeyCode::NumpadEnter) => {
          self.queue.push(InputFieldEvent::EnterPressed);
        }
        Some(VirtualKeyCode::Back) => {
          if !modifiers.ctrl {
            if self.has_selected() {

M src/ui/element/mod.rs => src/ui/element/mod.rs +0 -6
@@ 8,18 8,12 @@ pub use viewport::*;
use crate::ui::graphics::{DrawPreview, Graphics};
use crate::ui::rescap::ResizeCapabilities;
use crate::ui::util::is_in_bounds;
use crate::util::Color;

mod button;
mod spacer;
mod viewport;
mod inputfield;

pub static NORMAL_COLOR: Color = Color::from_rgb8(0x00, 0x82, 0xaa);
pub static ACTIVE_COLOR: Color = Color::from_rgb8(0x82, 0x00, 0xaa);
pub static INACTIVE_COLOR: Color = Color::from_rgb8(0x66, 0x66, 0x66);
pub static HIGHLIGHTED_COLOR: Color = Color::from_rgb8(0x00, 0xc4, 0xff);

pub trait Element {
  fn update(&mut self) {}


M src/ui/graphics.rs => src/ui/graphics.rs +6 -1
@@ 8,6 8,7 @@ use crate::font::bdf::draw::BakedFont;
use crate::font::FontRenderer;
use crate::math::{Mat4, vec3, Vec3};
use crate::shader::ShaderManager;
use crate::ui::settings::UiSettings;
use crate::util::{AnySurface, Color};

pub struct Graphics<'a, 'b, 'c> {


@@ 17,6 18,7 @@ pub struct Graphics<'a, 'b, 'c> {
  sm: &'a mut ShaderManager<'c>,
  perspective: Mat4,
  modelview: Vec<Transformation>,
  settings: UiSettings,

  // for batched rendering
  buffered_strings: Vec<(String, f32, f32, Color, Transformation)>,


@@ 24,7 26,7 @@ pub struct Graphics<'a, 'b, 'c> {
}

impl<'a, 'b, 'c> Graphics<'a, 'b, 'c> {
  pub fn new(facade: &'a dyn Facade, surface: &'a mut AnySurface<'b>, fr: &'a BakedFont<'a>, sm: &'a mut ShaderManager<'c>, perspective: Mat4) -> Self {
  pub fn new(facade: &'a dyn Facade, surface: &'a mut AnySurface<'b>, fr: &'a BakedFont<'a>, sm: &'a mut ShaderManager<'c>, perspective: Mat4, settings: UiSettings) -> Self {
    Graphics {
      facade,
      surface,


@@ 32,6 34,7 @@ impl<'a, 'b, 'c> Graphics<'a, 'b, 'c> {
      sm,
      perspective,
      modelview: vec![Transformation::Identity],
      settings,
      buffered_strings: vec![],
      buffered_tris: vec![],
    }


@@ 51,6 54,8 @@ impl<'a, 'b, 'c> Graphics<'a, 'b, 'c> {

  pub fn mvp(&self) -> Mat4 { *self.perspective() * self.modelview().as_matrix() }

  pub fn settings(&self) -> &UiSettings { &self.settings }

  pub fn transform(&mut self, tr: Transformation) {
    let m = self.modelview.last_mut().unwrap();
    *m = m.combine(tr);

M src/ui/mod.rs => src/ui/mod.rs +1 -0
@@ 16,6 16,7 @@ pub mod graphics;
pub mod event;
pub mod host;
pub mod controller;
pub mod settings;
pub mod util;

pub struct GenUi<E: UiAccess, C: UiController> {

A src/ui/settings.rs => src/ui/settings.rs +13 -0
@@ 0,0 1,13 @@
use crate::util::Color;

#[derive(Default, Copy, Clone, Debug)]
pub struct UiSettings {
  pub border_color: Color,
  pub normal_color: Color,
  pub highlighted_color: Color,
  pub active_color: Color,
  pub disabled_color: Color,
  pub query_text_color: Color,
  pub text_color: Color,
  pub text_selected_background_color: Color,
}
\ No newline at end of file

M src/util/color.rs => src/util/color.rs +12 -7
@@ 1,4 1,4 @@
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[derive(Default, Debug, Clone, Copy, Eq, PartialEq)]
pub struct Color {
  r: u8,
  g: u8,


@@ 11,14 11,19 @@ impl Color {
    Self::from_rgb8(255, 255, 255)
  }

  pub fn from_packed_rgba(rgba: u32) -> Self {
    let [b, g, r, a] = rgba.to_le_bytes();
    Self::from_rgba8(r, g, b, a)
  pub const fn from_packed_rgba(rgba: u32) -> Self {
    let a = rgba >> 24 & 0xFF;
    let r = rgba >> 16 & 0xFF;
    let g = rgba >> 8 & 0xFF;
    let b = rgba & 0xFF;
    Self::from_rgba8(r as u8, g as u8, b as u8, a as u8)
  }

  pub fn from_packed_rgb(rgb: u32) -> Self {
    let [b, g, r, _] = rgb.to_le_bytes();
    Self::from_rgb8(r, g, b)
  pub const fn from_packed_rgb(rgb: u32) -> Self {
    let r = rgb >> 16 & 0xFF;
    let g = rgb >> 8 & 0xFF;
    let b = rgb & 0xFF;
    Self::from_rgb8(r as u8, g as u8, b as u8)
  }

  pub const fn from_rgb8(r: u8, g: u8, b: u8) -> Self {

M src/util/mod.rs => src/util/mod.rs +5 -0
@@ 12,3 12,8 @@ mod math;
mod multiwrite;
mod rectmap;
pub mod single;

pub fn apply<T>(mut t: T, op: impl FnOnce(&mut T)) -> T {
  op(&mut t);
  t
}
\ No newline at end of file