dwarf_term/
lib.rs

1#![warn(missing_docs)]
2
3//! The `dwarf-term` crate allows you to draw a screen that ends up looking like
4//! dwarf fortress.
5//!
6//! At the moment there's only a single graphics set supported, but perhaps in
7//! future versions that will become an element that you can change at runtime.
8
9extern crate retro_pixel;
10pub use retro_pixel::*;
11
12#[macro_use]
13extern crate glium;
14use glium::{
15  backend::glutin::DisplayCreationError, glutin::{
16    dpi::{LogicalPosition, LogicalSize}, ContextBuilder, EventsLoop, WindowBuilder,
17  },
18  program::ProgramCreationError,
19  texture::{
20    texture2d::Texture2d, unsigned_texture2d::UnsignedTexture2d, ClientFormat, MipmapsOption, RawImage2d, TextureCreationError,
21    UncompressedFloatFormat,
22  },
23  uniforms::{MagnifySamplerFilter, MinifySamplerFilter, Sampler}, vertex::BufferCreationError, Display, DrawError, Program, Surface,
24  SwapBuffersError, VertexBuffer,
25};
26
27pub use glium::glutin::*;
28
29// std
30use std::borrow::Cow;
31
32// crate
33mod demo_font;
34use demo_font::*;
35
36#[derive(Copy, Clone)]
37struct Vertex {
38  position: [f32; 2],
39}
40implement_vertex!(Vertex, position);
41
42static VERTICES: [Vertex; 6] = [
43  Vertex { position: [-1.0, -1.0] },
44  Vertex { position: [-1.0, 1.0] },
45  Vertex { position: [1.0, 1.0] },
46  Vertex { position: [1.0, 1.0] },
47  Vertex { position: [1.0, -1.0] },
48  Vertex { position: [-1.0, -1.0] },
49];
50
51/// Some sort of OpenGL error happened.
52///
53/// If it did, this is probably my fault.
54#[derive(Debug)]
55#[allow(missing_docs)]
56pub enum DwarfTermGliumError {
57  DisplayCreationError(DisplayCreationError),
58  ProgramCreationError(ProgramCreationError),
59  TextureCreationError(TextureCreationError),
60  BufferCreationError(BufferCreationError),
61  DrawError(DrawError),
62  SwapBuffersError(SwapBuffersError),
63}
64
65/// A terminal where each cell has a foreground, background, and sprite id.
66pub struct DwarfTerm {
67  fgs: VecImage<u32>,
68  bgs: VecImage<u32>,
69  ids: VecImage<u8>,
70  clear_color: (f32, f32, f32, f32),
71  events_loop: EventsLoop,
72  display: Display,
73  program: Program,
74  sprite_texture: Texture2d,
75}
76impl ::std::fmt::Debug for DwarfTerm {
77  fn fmt(&self, f: &mut ::std::fmt::Formatter) -> Result<(), ::std::fmt::Error> {
78    write!(f, "DwarfTerm [ width {}, height {} ]", self.ids.width(), self.ids.height())
79  }
80}
81
82impl DwarfTerm {
83  /// Makes a new terminal of the given size and with the given title.
84  pub fn new<T>(grid_width: u32, grid_height: u32, title: T) -> Result<Self, DwarfTermGliumError>
85  where
86    T: Into<String>,
87  {
88    // Ready the Display we will use
89    let window_pixel_width = DEMO_FONT_TILE_SIZE.0 * grid_width;
90    let window_pixel_height = DEMO_FONT_TILE_SIZE.1 * grid_height;
91    let window_logical_size: LogicalSize = (window_pixel_width, window_pixel_height).into();
92    let events_loop = EventsLoop::new();
93    let window_builder = WindowBuilder::new()
94      .with_dimensions(window_logical_size)
95      .with_min_dimensions(window_logical_size)
96      .with_max_dimensions(window_logical_size)
97      .with_title(title);
98    let context_builder = ContextBuilder::new().with_vsync(true);
99    let display = Display::new(window_builder, context_builder, &events_loop).map_err(|e| DwarfTermGliumError::DisplayCreationError(e))?;
100    display.gl_window().set_position(LogicalPosition::new(500.0, 200.0));
101
102    // Ready the Program we will use
103    let program = Program::from_source(&display, VERT_SRC, FRAG_SRC, None).map_err(|e| DwarfTermGliumError::ProgramCreationError(e))?;
104
105    // Ready the sprite sheet
106    let raw_image = RawImage2d::from_raw_rgba_reversed(&DEMO_FONT, DEMO_FONT_SHEET_SIZE);
107    let sprite_texture = Texture2d::with_format(
108      &display,
109      raw_image,
110      UncompressedFloatFormat::U8U8U8U8,
111      MipmapsOption::AutoGeneratedMipmaps,
112    ).map_err(|e| DwarfTermGliumError::TextureCreationError(e))?;
113
114    Ok(Self {
115      fgs: VecImage::new(grid_width as usize, grid_height as usize),
116      bgs: VecImage::new(grid_width as usize, grid_height as usize),
117      ids: VecImage::new(grid_width as usize, grid_height as usize),
118      events_loop,
119      display,
120      program,
121      clear_color: (1.0, 1.0, 1.0, 1.0),
122      sprite_texture,
123    })
124  }
125
126  /// Sets the foreground color of every cell to be the given value.
127  pub fn set_all_foregrounds(&mut self, rgba: u32) {
128    self.fgs.set_all(rgba);
129  }
130
131  /// Sets the foreground color of every cell to be the given value.
132  pub fn set_all_backgrounds(&mut self, rgba: u32) {
133    self.bgs.set_all(rgba);
134  }
135
136  /// Sets the tile id of every cell to be the given value.
137  pub fn set_all_ids(&mut self, tile_id: u8) {
138    self.ids.set_all(tile_id);
139  }
140
141  /// Sets the "clear" color that's used.
142  ///
143  /// This just passes the call along to OpenGL. Inputs should each be in the
144  /// 0.0 to 1.0 (inclusive) range. Out of bounds inputs are automatically
145  /// clamped by OpenGL.
146  pub fn set_clear_color(&mut self, r: f32, g: f32, b: f32, a: f32) {
147    self.clear_color = (r, g, b, a);
148  }
149
150  /// gets a mutable reference to the foreground at the position specified.
151  ///
152  /// Fails if the position would be out of bounds.
153  pub fn get_foreground_mut(&mut self, position: (usize, usize)) -> Option<&mut u32> {
154    self.fgs.get_mut(position)
155  }
156
157  /// gets a mutable reference to the background at the position specified.
158  ///
159  /// Fails if the position would be out of bounds.
160  pub fn get_background_mut(&mut self, position: (usize, usize)) -> Option<&mut u32> {
161    self.bgs.get_mut(position)
162  }
163
164  /// gets a mutable reference to the tile id at the position specified.
165  ///
166  /// Fails if the position would be out of bounds.
167  pub fn get_id_mut(&mut self, position: (usize, usize)) -> Option<&mut u8> {
168    self.ids.get_mut(position)
169  }
170
171  /// Lets you poll for events on the window.
172  ///
173  /// The DwarfTerm will automatically check for `WindowEvent::Resized` events
174  /// and then keep itself up to date. However, it will _also_ pass along such
175  /// events to your callback so that you can make any necessary changes on your
176  /// end as well.
177  pub fn poll_events<F>(&mut self, mut callback: F)
178  where
179    F: FnMut(Event),
180  {
181    self.events_loop.poll_events(|event| {
182      callback(event);
183    });
184  }
185
186  /// Gives back all three layers at once in image slice form.
187  ///
188  /// (fgs, bgs, ids).
189  pub fn layer_slices(&self) -> (ImageSlice<u32>, ImageSlice<u32>, ImageSlice<u8>) {
190    let zero = (0, 0);
191    let fg_extent = (self.fgs.width(), self.fgs.height());
192    let bg_extent = (self.bgs.width(), self.bgs.height());
193    let id_extent = (self.ids.width(), self.ids.height());
194    (
195      self.fgs.slice(zero..fg_extent),
196      self.bgs.slice(zero..bg_extent),
197      self.ids.slice(zero..id_extent),
198    )
199  }
200
201  /// Gives back all three layers at once in image mutable slice form.
202  ///
203  /// (fgs, bgs, ids).
204  pub fn layer_slices_mut(&mut self) -> (ImageMutSlice<u32>, ImageMutSlice<u32>, ImageMutSlice<u8>) {
205    let zero = (0, 0);
206    let fg_extent = (self.fgs.width(), self.fgs.height());
207    let bg_extent = (self.bgs.width(), self.bgs.height());
208    let id_extent = (self.ids.width(), self.ids.height());
209    (
210      self.fgs.slice_mut(zero..fg_extent),
211      self.bgs.slice_mut(zero..bg_extent),
212      self.ids.slice_mut(zero..id_extent),
213    )
214  }
215
216  /// Clears the old screen, draws the new data, and then swaps the buffers.
217  ///
218  /// This should hopefully never error! But it might. If it does, there's a
219  /// strong chance it's my fault, not yours.
220  pub fn clear_draw_swap(&mut self) -> Result<(), DwarfTermGliumError> {
221    // double check things
222    debug_assert_eq!(self.ids.as_ref().len(), self.ids.width() * self.ids.height());
223    debug_assert_eq!(self.fgs.as_ref().len(), self.fgs.width() * self.fgs.height());
224    debug_assert_eq!(self.bgs.as_ref().len(), self.bgs.width() * self.bgs.height());
225
226    let the_resolution = {
227      let logical_size = self.display.gl_window().window().get_inner_size().unwrap();
228      [logical_size.width, logical_size.height]
229    };
230    let the_grid_count_f32 = [self.ids.width() as f32, self.ids.height() as f32];
231    let the_tile_id_texture = UnsignedTexture2d::new(
232      &self.display,
233      RawImage2d {
234        data: Cow::Borrowed(self.ids.as_ref()),
235        width: self.ids.width() as u32,
236        height: self.ids.height() as u32,
237        format: ClientFormat::U8,
238      },
239    ).map_err(|e| DwarfTermGliumError::TextureCreationError(e))?;
240    let the_tile_foreground_texture = Texture2d::new(
241      &self.display,
242      RawImage2d {
243        data: Cow::Borrowed(self.fgs.as_ref()),
244        width: self.fgs.width() as u32,
245        height: self.fgs.height() as u32,
246        format: ClientFormat::U8U8U8U8,
247      },
248    ).map_err(|e| DwarfTermGliumError::TextureCreationError(e))?;
249    let the_tile_background_texture = Texture2d::new(
250      &self.display,
251      RawImage2d {
252        data: Cow::Borrowed(self.bgs.as_ref()),
253        width: self.bgs.width() as u32,
254        height: self.bgs.height() as u32,
255        format: ClientFormat::U8U8U8U8,
256      },
257    ).map_err(|e| DwarfTermGliumError::TextureCreationError(e))?;
258    let vertex_buffer = VertexBuffer::new(&self.display, &VERTICES).map_err(|e| DwarfTermGliumError::BufferCreationError(e))?;
259    let indices = glium::index::NoIndices(glium::index::PrimitiveType::TrianglesList);
260    let uniforms = uniform!{
261      screen_resolution: the_resolution,
262      tile_size: DEMO_FONT_TILE_SIZE_VEC2,
263      grid_count: the_grid_count_f32,
264      tile_sheet_texture: Sampler::new(&self.sprite_texture)
265        .minify_filter(MinifySamplerFilter::Nearest)
266        .magnify_filter(MagnifySamplerFilter::Nearest),
267      tile_id_texture: Sampler::new(&the_tile_id_texture)
268        .minify_filter(MinifySamplerFilter::Nearest)
269        .magnify_filter(MagnifySamplerFilter::Nearest),
270      tile_foreground_texture: Sampler::new(&the_tile_foreground_texture)
271        .minify_filter(MinifySamplerFilter::Nearest)
272        .magnify_filter(MagnifySamplerFilter::Nearest),
273      tile_background_texture: Sampler::new(&the_tile_background_texture)
274        .minify_filter(MinifySamplerFilter::Nearest)
275        .magnify_filter(MagnifySamplerFilter::Nearest),
276    };
277
278    let mut target = self.display.draw();
279    target.clear_color(self.clear_color.0, self.clear_color.1, self.clear_color.2, self.clear_color.3);
280    // Note(Lokathor): We CANNOT return early using `?` once there's a Surface
281    // in play, or the Drop impl of the drawing target will make the whole
282    // program panic since we didn't explicitly call `target.finish()`. I'm
283    // pretty sure it's stupid like that because `?` was added after Glium
284    // stopped major work, but there you go.
285    match target.draw(&vertex_buffer, &indices, &self.program, &uniforms, &Default::default()) {
286      Ok(()) => target.finish().map_err(|e| DwarfTermGliumError::SwapBuffersError(e)),
287      Err(draw_error) => {
288        target.finish().ok();
289        Err(DwarfTermGliumError::DrawError(draw_error))
290      }
291    }
292  }
293}
294
295//
296// SHADER STUFF
297//
298
299static VERT_SRC: &'static str = include_str!("dwarf.vert");
300
301static FRAG_SRC: &'static str = include_str!("dwarf.frag");