1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
// This example shows pretty much a complete rbop usage. It implements its own renderer for drawing
// onto a Speedy2D canvas, accepts input, and evaluates the result.
//
// If you are reading these examples to become familiar with rbop, it is recommended that you read
// the `ascii_calc` example first, as that will contain a more thorough description of handling
// input and evaluation. This will focus more on the implementation of the renderer.
#![feature(box_syntax)]
#![feature(result_flattening)]
// Enforce `examples` feature is passed ------------------------------------------------------------
#[cfg(not(feature = "examples"))]
mod window_calc {
pub const CHECK: usize = panic!("you must enable the `examples` feature to compile examples.");
}
#[cfg(not(feature = "examples"))]
fn main() {
println!("{}", window_calc::CHECK);
}
// -------------------------------------------------------------------------------------------------
#[cfg(feature = "examples")]
mod window_calc {
use std::rc::Rc;
use rbop::{Token, UnstructuredNode, UnstructuredNodeList, node::{unstructured::{UnstructuredNodeRoot, Upgradable}, structured::EvaluationSettings}, render::{Renderer, SizedGlyph, ViewportGlyph}};
use speedy2d::{self, Graphics2D, Window, color::Color, font::{Font, FormattedTextBlock, TextLayout, TextOptions}, window::{VirtualKeyCode, WindowHandler, WindowHelper}};
// This is the struct we'll implement `Renderer` on! The fields will be very
// implementation-specific, here we're holding a reference to the Speedy2D graphics surface and
// font.
struct Speedy2DRenderer<'a> {
graphics: Option<&'a mut Graphics2D>,
font: Font,
}
impl<'a> Speedy2DRenderer<'a> {
/// Uses this renderer's font to lay out the given `text` into a `FormattedTextBlock`.
fn text_layout(&mut self, text: &str, size_reduction_level: u32) -> Rc<FormattedTextBlock> {
// Half the default text size for each level of size reduction
let mut size = 50.0;
for _ in 0..size_reduction_level {
size /= 2.0;
}
self.font.layout_text(text, size, TextOptions::new())
}
/// Returns the size of the given `text when rendered using this renderer's font.
fn text_size(&mut self, text: &str, size_reduction_level: u32) -> rbop::render::Area {
let layout = self.text_layout(text, size_reduction_level);
rbop::render::Area {
width: layout.width() as u64,
height: layout.height() as u64,
}
}
/// Draws `text` onto the graphics surface at `point`, using this renderer's font.
fn text_draw(&mut self, text: &str, point: rbop::render::ViewportPoint, size_reduction_level: u32) {
let layout = &self.text_layout(text, size_reduction_level);
self.graphics.as_mut().unwrap().draw_text(
(point.x as f32, point.y as f32),
Color::BLACK,
layout
);
}
}
// The implementation which will allow this struct to be used to render rbop expressions!
// There are only three methods which need to be implemented...
impl<'a> Renderer for Speedy2DRenderer<'a> {
// The `init` method is called by `draw_all` after computing the layout, but before drawing
// any glyphs. It can be used to perform any pre-draw setup required; here, that is clearing
// the screen.
fn init(&mut self, _size: rbop::render::Area) {
self.graphics.as_mut().unwrap().clear_screen(Color::from_rgb(1.0, 1.0, 1.0));
}
// The `size` method is used during layout computation. rbop needs to know the size of each
// glyphs drawn with this renderer, so this method takes a glyph and returns its size.
//
// There is no hard requirement that this actually matches the size of the glyphs drawn to
// the screen; for example, we lie that the cursor has a width of 0, to stop the glyphs
// around the cursor wobbling when it is moved.
//
// The size reduction level starts from 0, and increases by one each time a node is nested
// inside another in such a way where the resulting glyph should be drawn smaller. For
// example, when you're writing a power, you tend to write the exponent smaller than the
// base - so the exponent would have 1 greater size reduction level than the base. You can
// completely ignore this if you like! (The built-in AsciiRenderer does - ASCII doesn't
// give us enough resolution to decrease the size of an exponent.)
fn size(&mut self, glyph: rbop::render::Glyph, size_reduction_level: u32) -> rbop::render::Area {
match glyph {
rbop::render::Glyph::Digit { number } =>
self.text_size(&format!("{}", number), size_reduction_level),
rbop::render::Glyph::Add => self.text_size("+", size_reduction_level),
rbop::render::Glyph::Subtract => self.text_size("-", size_reduction_level),
rbop::render::Glyph::Multiply => self.text_size("*", size_reduction_level),
rbop::render::Glyph::Divide => self.text_size("/", size_reduction_level),
rbop::render::Glyph::Fraction { inner_width } => rbop::render::Area {
width: inner_width,
height: 3,
},
rbop::render::Glyph::Cursor { height } => rbop::render::Area {
// Lie about the cursor width! This means that rbop doesn't make space for the
// cursor, so the cursor moving won't cause elements to shift a little bit
width: 0,
height,
},
rbop::render::Glyph::Placeholder => self.text_size("X", size_reduction_level),
// TODO: not everything's implemented
rbop::render::Glyph::LeftParenthesis { .. } => todo!(),
rbop::render::Glyph::RightParenthesis { .. } => todo!(),
rbop::render::Glyph::Sqrt { .. } => todo!(),
rbop::render::Glyph::Point => todo!(),
rbop::render::Glyph::Variable { .. } => todo!(),
rbop::render::Glyph::FunctionName { .. } => todo!(),
rbop::render::Glyph::Comma => todo!(),
}
}
// After the layout has been computed, this `draw` method will be called for every glyph.
// The implementation of this method should draw the passed glyph to the given point.
fn draw(&mut self, viewport_glyph: rbop::render::ViewportGlyph) {
// Unpack the given `ViewportGlyph`. These encode quite a bit of information:
// - Which glyph it actually is
// - The position of the glyph
// - How big the glyph is
// - How much of the glyph is visible within the viewport, if present
let ViewportGlyph {
glyph: SizedGlyph { glyph, size_reduction_level, .. },
point,
..
} = viewport_glyph;
// Offset a little bit from the origin
let point = point.dx(20).dy(20);
// Match on the glyph to draw
match glyph {
rbop::render::Glyph::Digit { number } =>
self.text_draw(&format!("{}", number), point, size_reduction_level),
rbop::render::Glyph::Add => self.text_draw("+", point, size_reduction_level),
rbop::render::Glyph::Subtract => self.text_draw("-", point, size_reduction_level),
rbop::render::Glyph::Multiply => self.text_draw("*", point, size_reduction_level),
rbop::render::Glyph::Divide => self.text_draw("/", point, size_reduction_level),
rbop::render::Glyph::Fraction { inner_width } =>
self.graphics.as_mut().unwrap().draw_line(
(point.x as f32, point.y as f32),
(point.x as f32 + inner_width as f32, point.y as f32),
3.0,
Color::BLACK
),
rbop::render::Glyph::Cursor { height } =>
self.graphics.as_mut().unwrap().draw_line(
(point.x as f32, point.y as f32),
(point.x as f32, point.y as f32 + height as f32),
1.0,
Color::BLACK
),
rbop::render::Glyph::Placeholder => self.text_draw("?", point, size_reduction_level),
// TODO: not everything's implemented
rbop::render::Glyph::LeftParenthesis { .. } => todo!(),
rbop::render::Glyph::RightParenthesis { .. } => todo!(),
rbop::render::Glyph::Sqrt { .. } => todo!(),
rbop::render::Glyph::Point => todo!(),
rbop::render::Glyph::Variable { .. } => todo!(),
rbop::render::Glyph::FunctionName { .. } => todo!(),
rbop::render::Glyph::Comma => todo!(),
}
}
}
/// The Speedy2D window handler implementation. This also contains the required pieces of the
/// rbop context, except the renderer - renderer instances are created on-the-fly, trading off
/// performance for "borrow checker sanity" :P
struct WindowCalc {
root: UnstructuredNodeRoot,
nav_path: rbop::nav::NavPath,
needs_draw: bool,
}
impl WindowCalc {
/// Create a new window.
fn new_window() -> Window {
Window::new_centered("Window Calc", (640, 480))
.expect("unable to create window")
}
#[cfg(target_os = "linux")]
fn font() -> &'static [u8] {
include_bytes!("/usr/share/fonts/Arial.ttf")
}
#[cfg(target_os = "macos")]
fn font() -> &'static [u8] {
include_bytes!("/System/Library/Fonts/Supplemental/Arial.ttf")
}
/// Create a new `Speedy2DRenderer` using the given graphics surface.
fn create_renderer<'a>(&mut self, graphics: Option<&'a mut Graphics2D>) -> Speedy2DRenderer<'a> {
Speedy2DRenderer {
graphics,
font: Font::new(WindowCalc::font()).unwrap(),
}
}
}
impl WindowHandler for WindowCalc {
fn on_draw(&mut self, helper: &mut WindowHelper, graphics: &mut Graphics2D) {
// Only draw the screen if there was an rbop input since the last draw
if self.needs_draw {
graphics.clear_screen(Color::from_rgb(1.0, 1.0, 1.0));
// A bit funky, but makes the borrow checker happy
// Means that we drop create_renderer's mutable borrow before using `draw_text` again
let result_text = {
let mut renderer = self.create_renderer(Some(graphics));
renderer.draw_all(&self.root, Some(&mut self.nav_path.to_navigator()), None);
let result = self.root.upgrade().map(|x| x.evaluate(&EvaluationSettings::default()));
renderer.text_layout(&match result {
Ok(Ok(number)) => format!("{:?}", number),
Ok(Err(error)) => error.to_string(),
Err(error) => error.to_string(),
}, 0)
};
graphics.draw_text(
(20.0, 400.0),
Color::BLACK,
&result_text,
);
self.needs_draw = false;
}
helper.request_redraw();
}
fn on_key_down(&mut self, _helper: &mut WindowHelper<()>, virtual_key_code: Option<VirtualKeyCode>, _scancode: speedy2d::window::KeyScancode) {
// Create a new renderer - not bound to a particular graphics surface since it's only
// used for its `size` method
let mut renderer = self.create_renderer(None);
// Handle the input key
let node_to_insert = match virtual_key_code.unwrap() {
VirtualKeyCode::Key0 => Some(UnstructuredNode::Token(Token::Digit(0))),
VirtualKeyCode::Key1 => Some(UnstructuredNode::Token(Token::Digit(1))),
VirtualKeyCode::Key2 => Some(UnstructuredNode::Token(Token::Digit(2))),
VirtualKeyCode::Key3 => Some(UnstructuredNode::Token(Token::Digit(3))),
VirtualKeyCode::Key4 => Some(UnstructuredNode::Token(Token::Digit(4))),
VirtualKeyCode::Key5 => Some(UnstructuredNode::Token(Token::Digit(5))),
VirtualKeyCode::Key6 => Some(UnstructuredNode::Token(Token::Digit(6))),
VirtualKeyCode::Key7 => Some(UnstructuredNode::Token(Token::Digit(7))),
VirtualKeyCode::Key8 => Some(UnstructuredNode::Token(Token::Digit(8))),
VirtualKeyCode::Key9 => Some(UnstructuredNode::Token(Token::Digit(9))),
VirtualKeyCode::Plus => Some(UnstructuredNode::Token(Token::Add)),
VirtualKeyCode::Minus => Some(UnstructuredNode::Token(Token::Subtract)),
VirtualKeyCode::Asterisk => Some(UnstructuredNode::Token(Token::Multiply)),
VirtualKeyCode::Slash => Some(UnstructuredNode::Fraction(
UnstructuredNodeList::new(),
UnstructuredNodeList::new(),
)),
VirtualKeyCode::S => Some(UnstructuredNode::Sqrt(
UnstructuredNodeList::new(),
)),
VirtualKeyCode::P => Some(UnstructuredNode::Power(
UnstructuredNodeList::new(),
)),
VirtualKeyCode::Left => { self.root.move_left(&mut self.nav_path, &mut renderer, None); None }
VirtualKeyCode::Right => { self.root.move_right(&mut self.nav_path, &mut renderer, None); None }
VirtualKeyCode::Down => { self.root.move_down(&mut self.nav_path, &mut renderer, None); None }
VirtualKeyCode::Up => { self.root.move_up(&mut self.nav_path, &mut renderer, None); None }
VirtualKeyCode::Backspace => { self.root.delete(&mut self.nav_path, &mut renderer, None); None }
_ => return,
};
if let Some(node) = node_to_insert {
self.root.insert(
&mut self.nav_path,
&mut renderer,
None,
node,
);
}
self.needs_draw = true;
}
}
pub fn main() {
WindowCalc::new_window().run_loop(WindowCalc {
root: UnstructuredNodeRoot::new(),
nav_path: rbop::nav::NavPath::new(vec![0]),
needs_draw: true,
})
}
}
#[cfg(feature = "examples")]
fn main() {
window_calc::main();
}