Skip to main content

endbasic_std/gfx/
mod.rs

1// EndBASIC
2// Copyright 2021 Julio Merino
3//
4// Licensed under the Apache License, Version 2.0 (the "License"); you may not
5// use this file except in compliance with the License.  You may obtain a copy
6// of the License at:
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
13// License for the specific language governing permissions and limitations
14// under the License.
15
16//! Commands for graphical console interaction.
17
18use crate::console::{Console, PixelsXY};
19use async_trait::async_trait;
20use endbasic_core::LineCol;
21use endbasic_core::ast::{ArgSep, ExprType};
22use endbasic_core::compiler::{ArgSepSyntax, RequiredValueSyntax, SingularArgSyntax};
23use endbasic_core::exec::{Error, Machine, Result, Scope};
24use endbasic_core::syms::{Callable, CallableMetadata, CallableMetadataBuilder};
25use std::borrow::Cow;
26use std::cell::RefCell;
27use std::convert::TryFrom;
28use std::rc::Rc;
29
30pub mod lcd;
31
32/// Category description for all symbols provided by this module.
33const CATEGORY: &str = "Graphics
34The EndBASIC console overlays text and graphics in the same canvas.  The consequence of this \
35design choice is that the console has two coordinate systems: the character-based system, used by
36the commands described in HELP \"CONSOLE\", and the pixel-based system, used by the commands \
37described in this section.";
38
39/// Parses an expression that represents a single coordinate.
40fn parse_coordinate(i: i32, pos: LineCol) -> Result<i16> {
41    match i16::try_from(i) {
42        Ok(i) => Ok(i),
43        Err(_) => Err(Error::SyntaxError(pos, format!("Coordinate {} out of range", i))),
44    }
45}
46
47/// Parses a pair of expressions that represent an (x,y) coordinate pair.
48fn parse_coordinates(xvalue: i32, xpos: LineCol, yvalue: i32, ypos: LineCol) -> Result<PixelsXY> {
49    Ok(PixelsXY { x: parse_coordinate(xvalue, xpos)?, y: parse_coordinate(yvalue, ypos)? })
50}
51
52/// Parses an expression that represents a radius.
53fn parse_radius(i: i32, pos: LineCol) -> Result<u16> {
54    match u16::try_from(i) {
55        Ok(i) => Ok(i),
56        Err(_) if i < 0 => Err(Error::SyntaxError(pos, format!("Radius {} must be positive", i))),
57        Err(_) => Err(Error::SyntaxError(pos, format!("Radius {} out of range", i))),
58    }
59}
60
61/// The `GFX_CIRCLE` command.
62pub struct GfxCircleCommand {
63    metadata: CallableMetadata,
64    console: Rc<RefCell<dyn Console>>,
65}
66
67impl GfxCircleCommand {
68    /// Creates a new `GFX_CIRCLE` command that draws an empty circle on `console`.
69    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
70        Rc::from(Self {
71            metadata: CallableMetadataBuilder::new("GFX_CIRCLE")
72                .with_syntax(&[(
73                    &[
74                        SingularArgSyntax::RequiredValue(
75                            RequiredValueSyntax {
76                                name: Cow::Borrowed("x"),
77                                vtype: ExprType::Integer,
78                            },
79                            ArgSepSyntax::Exactly(ArgSep::Long),
80                        ),
81                        SingularArgSyntax::RequiredValue(
82                            RequiredValueSyntax {
83                                name: Cow::Borrowed("y"),
84                                vtype: ExprType::Integer,
85                            },
86                            ArgSepSyntax::Exactly(ArgSep::Long),
87                        ),
88                        SingularArgSyntax::RequiredValue(
89                            RequiredValueSyntax {
90                                name: Cow::Borrowed("r"),
91                                vtype: ExprType::Integer,
92                            },
93                            ArgSepSyntax::End,
94                        ),
95                    ],
96                    None,
97                )])
98                .with_category(CATEGORY)
99                .with_description(
100                    "Draws a circle of radius r centered at (x,y).
101The outline of the circle is drawn using the foreground color as selected by COLOR and the \
102area of the circle is left untouched.",
103                )
104                .build(),
105            console,
106        })
107    }
108}
109
110#[async_trait(?Send)]
111impl Callable for GfxCircleCommand {
112    fn metadata(&self) -> &CallableMetadata {
113        &self.metadata
114    }
115
116    async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> {
117        debug_assert_eq!(3, scope.nargs());
118        let (xvalue, xpos) = scope.pop_integer_with_pos();
119        let (yvalue, ypos) = scope.pop_integer_with_pos();
120        let (rvalue, rpos) = scope.pop_integer_with_pos();
121
122        let xy = parse_coordinates(xvalue, xpos, yvalue, ypos)?;
123        let r = parse_radius(rvalue, rpos)?;
124
125        self.console.borrow_mut().draw_circle(xy, r).map_err(|e| scope.io_error(e))?;
126        Ok(())
127    }
128}
129
130/// The `GFX_CIRCLEF` command.
131pub struct GfxCirclefCommand {
132    metadata: CallableMetadata,
133    console: Rc<RefCell<dyn Console>>,
134}
135
136impl GfxCirclefCommand {
137    /// Creates a new `GFX_CIRCLEF` command that draws a filled circle on `console`.
138    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
139        Rc::from(Self {
140            metadata: CallableMetadataBuilder::new("GFX_CIRCLEF")
141                .with_syntax(&[(
142                    &[
143                        SingularArgSyntax::RequiredValue(
144                            RequiredValueSyntax {
145                                name: Cow::Borrowed("x"),
146                                vtype: ExprType::Integer,
147                            },
148                            ArgSepSyntax::Exactly(ArgSep::Long),
149                        ),
150                        SingularArgSyntax::RequiredValue(
151                            RequiredValueSyntax {
152                                name: Cow::Borrowed("y"),
153                                vtype: ExprType::Integer,
154                            },
155                            ArgSepSyntax::Exactly(ArgSep::Long),
156                        ),
157                        SingularArgSyntax::RequiredValue(
158                            RequiredValueSyntax {
159                                name: Cow::Borrowed("r"),
160                                vtype: ExprType::Integer,
161                            },
162                            ArgSepSyntax::End,
163                        ),
164                    ],
165                    None,
166                )])
167                .with_category(CATEGORY)
168                .with_description(
169                    "Draws a filled circle of radius r centered at (x,y).
170The outline and area of the circle are drawn using the foreground color as selected by COLOR.",
171                )
172                .build(),
173            console,
174        })
175    }
176}
177
178#[async_trait(?Send)]
179impl Callable for GfxCirclefCommand {
180    fn metadata(&self) -> &CallableMetadata {
181        &self.metadata
182    }
183
184    async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> {
185        debug_assert_eq!(3, scope.nargs());
186        let (xvalue, xpos) = scope.pop_integer_with_pos();
187        let (yvalue, ypos) = scope.pop_integer_with_pos();
188        let (rvalue, rpos) = scope.pop_integer_with_pos();
189
190        let xy = parse_coordinates(xvalue, xpos, yvalue, ypos)?;
191        let r = parse_radius(rvalue, rpos)?;
192
193        self.console.borrow_mut().draw_circle_filled(xy, r).map_err(|e| scope.io_error(e))?;
194        Ok(())
195    }
196}
197
198/// The `GFX_HEIGHT` function.
199pub struct GfxHeightFunction {
200    metadata: CallableMetadata,
201    console: Rc<RefCell<dyn Console>>,
202}
203
204impl GfxHeightFunction {
205    /// Creates a new instance of the function.
206    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
207        Rc::from(Self {
208            metadata: CallableMetadataBuilder::new("GFX_HEIGHT")
209                .with_return_type(ExprType::Integer)
210                .with_syntax(&[(&[], None)])
211                .with_category(CATEGORY)
212                .with_description(
213                    "Returns the height in pixels of the graphical console.
214See GFX_WIDTH to query the other dimension.",
215                )
216                .build(),
217            console,
218        })
219    }
220}
221
222#[async_trait(?Send)]
223impl Callable for GfxHeightFunction {
224    fn metadata(&self) -> &CallableMetadata {
225        &self.metadata
226    }
227
228    async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> Result<()> {
229        debug_assert_eq!(0, scope.nargs());
230        let size = self.console.borrow().size_pixels().map_err(|e| scope.io_error(e))?;
231        scope.return_integer(i32::from(size.height))
232    }
233}
234
235/// The `GFX_LINE` command.
236pub struct GfxLineCommand {
237    metadata: CallableMetadata,
238    console: Rc<RefCell<dyn Console>>,
239}
240
241impl GfxLineCommand {
242    /// Creates a new `GFX_LINE` command that draws a line on `console`.
243    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
244        Rc::from(Self {
245            metadata: CallableMetadataBuilder::new("GFX_LINE")
246                .with_syntax(&[(
247                    &[
248                        SingularArgSyntax::RequiredValue(
249                            RequiredValueSyntax {
250                                name: Cow::Borrowed("x1"),
251                                vtype: ExprType::Integer,
252                            },
253                            ArgSepSyntax::Exactly(ArgSep::Long),
254                        ),
255                        SingularArgSyntax::RequiredValue(
256                            RequiredValueSyntax {
257                                name: Cow::Borrowed("y1"),
258                                vtype: ExprType::Integer,
259                            },
260                            ArgSepSyntax::Exactly(ArgSep::Long),
261                        ),
262                        SingularArgSyntax::RequiredValue(
263                            RequiredValueSyntax {
264                                name: Cow::Borrowed("x2"),
265                                vtype: ExprType::Integer,
266                            },
267                            ArgSepSyntax::Exactly(ArgSep::Long),
268                        ),
269                        SingularArgSyntax::RequiredValue(
270                            RequiredValueSyntax {
271                                name: Cow::Borrowed("y2"),
272                                vtype: ExprType::Integer,
273                            },
274                            ArgSepSyntax::End,
275                        ),
276                    ],
277                    None,
278                )])
279                .with_category(CATEGORY)
280                .with_description(
281                    "Draws a line from (x1,y1) to (x2,y2).
282The line is drawn using the foreground color as selected by COLOR.",
283                )
284                .build(),
285            console,
286        })
287    }
288}
289
290#[async_trait(?Send)]
291impl Callable for GfxLineCommand {
292    fn metadata(&self) -> &CallableMetadata {
293        &self.metadata
294    }
295
296    async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> {
297        debug_assert_eq!(4, scope.nargs());
298        let (x1value, x1pos) = scope.pop_integer_with_pos();
299        let (y1value, y1pos) = scope.pop_integer_with_pos();
300        let (x2value, x2pos) = scope.pop_integer_with_pos();
301        let (y2value, y2pos) = scope.pop_integer_with_pos();
302
303        let x1y1 = parse_coordinates(x1value, x1pos, y1value, y1pos)?;
304        let x2y2 = parse_coordinates(x2value, x2pos, y2value, y2pos)?;
305
306        self.console.borrow_mut().draw_line(x1y1, x2y2).map_err(|e| scope.io_error(e))?;
307        Ok(())
308    }
309}
310
311/// The `GFX_PIXEL` command.
312pub struct GfxPixelCommand {
313    metadata: CallableMetadata,
314    console: Rc<RefCell<dyn Console>>,
315}
316
317impl GfxPixelCommand {
318    /// Creates a new `GFX_PIXEL` command that draws a single pixel on `console`.
319    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
320        Rc::from(Self {
321            metadata: CallableMetadataBuilder::new("GFX_PIXEL")
322                .with_syntax(&[(
323                    &[
324                        SingularArgSyntax::RequiredValue(
325                            RequiredValueSyntax {
326                                name: Cow::Borrowed("x"),
327                                vtype: ExprType::Integer,
328                            },
329                            ArgSepSyntax::Exactly(ArgSep::Long),
330                        ),
331                        SingularArgSyntax::RequiredValue(
332                            RequiredValueSyntax {
333                                name: Cow::Borrowed("y"),
334                                vtype: ExprType::Integer,
335                            },
336                            ArgSepSyntax::End,
337                        ),
338                    ],
339                    None,
340                )])
341                .with_category(CATEGORY)
342                .with_description(
343                    "Draws a pixel at (x,y).
344The pixel is drawn using the foreground color as selected by COLOR.",
345                )
346                .build(),
347            console,
348        })
349    }
350}
351
352#[async_trait(?Send)]
353impl Callable for GfxPixelCommand {
354    fn metadata(&self) -> &CallableMetadata {
355        &self.metadata
356    }
357
358    async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> {
359        debug_assert_eq!(2, scope.nargs());
360        let (xvalue, xpos) = scope.pop_integer_with_pos();
361        let (yvalue, ypos) = scope.pop_integer_with_pos();
362
363        let xy = parse_coordinates(xvalue, xpos, yvalue, ypos)?;
364
365        self.console.borrow_mut().draw_pixel(xy).map_err(|e| scope.io_error(e))?;
366        Ok(())
367    }
368}
369
370/// The `GFX_RECT` command.
371pub struct GfxRectCommand {
372    metadata: CallableMetadata,
373    console: Rc<RefCell<dyn Console>>,
374}
375
376impl GfxRectCommand {
377    /// Creates a new `GFX_RECT` command that draws an empty rectangle on `console`.
378    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
379        Rc::from(Self {
380            metadata: CallableMetadataBuilder::new("GFX_RECT")
381                .with_syntax(&[(
382                    &[
383                        SingularArgSyntax::RequiredValue(
384                            RequiredValueSyntax {
385                                name: Cow::Borrowed("x1"),
386                                vtype: ExprType::Integer,
387                            },
388                            ArgSepSyntax::Exactly(ArgSep::Long),
389                        ),
390                        SingularArgSyntax::RequiredValue(
391                            RequiredValueSyntax {
392                                name: Cow::Borrowed("y1"),
393                                vtype: ExprType::Integer,
394                            },
395                            ArgSepSyntax::Exactly(ArgSep::Long),
396                        ),
397                        SingularArgSyntax::RequiredValue(
398                            RequiredValueSyntax {
399                                name: Cow::Borrowed("x2"),
400                                vtype: ExprType::Integer,
401                            },
402                            ArgSepSyntax::Exactly(ArgSep::Long),
403                        ),
404                        SingularArgSyntax::RequiredValue(
405                            RequiredValueSyntax {
406                                name: Cow::Borrowed("y2"),
407                                vtype: ExprType::Integer,
408                            },
409                            ArgSepSyntax::End,
410                        ),
411                    ],
412                    None,
413                )])
414                .with_category(CATEGORY)
415                .with_description(
416                    "Draws a rectangle from (x1,y1) to (x2,y2).
417The outline of the rectangle is drawn using the foreground color as selected by COLOR and the \
418area of the rectangle is left untouched.",
419                )
420                .build(),
421            console,
422        })
423    }
424}
425
426#[async_trait(?Send)]
427impl Callable for GfxRectCommand {
428    fn metadata(&self) -> &CallableMetadata {
429        &self.metadata
430    }
431
432    async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> {
433        debug_assert_eq!(4, scope.nargs());
434        let (x1value, x1pos) = scope.pop_integer_with_pos();
435        let (y1value, y1pos) = scope.pop_integer_with_pos();
436        let (x2value, x2pos) = scope.pop_integer_with_pos();
437        let (y2value, y2pos) = scope.pop_integer_with_pos();
438
439        let x1y1 = parse_coordinates(x1value, x1pos, y1value, y1pos)?;
440        let x2y2 = parse_coordinates(x2value, x2pos, y2value, y2pos)?;
441
442        self.console.borrow_mut().draw_rect(x1y1, x2y2).map_err(|e| scope.io_error(e))?;
443        Ok(())
444    }
445}
446
447/// The `GFX_RECTF` command.
448pub struct GfxRectfCommand {
449    metadata: CallableMetadata,
450    console: Rc<RefCell<dyn Console>>,
451}
452
453impl GfxRectfCommand {
454    /// Creates a new `GFX_RECTF` command that draws a filled rectangle on `console`.
455    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
456        Rc::from(Self {
457            metadata: CallableMetadataBuilder::new("GFX_RECTF")
458                .with_syntax(&[(
459                    &[
460                        SingularArgSyntax::RequiredValue(
461                            RequiredValueSyntax {
462                                name: Cow::Borrowed("x1"),
463                                vtype: ExprType::Integer,
464                            },
465                            ArgSepSyntax::Exactly(ArgSep::Long),
466                        ),
467                        SingularArgSyntax::RequiredValue(
468                            RequiredValueSyntax {
469                                name: Cow::Borrowed("y1"),
470                                vtype: ExprType::Integer,
471                            },
472                            ArgSepSyntax::Exactly(ArgSep::Long),
473                        ),
474                        SingularArgSyntax::RequiredValue(
475                            RequiredValueSyntax {
476                                name: Cow::Borrowed("x2"),
477                                vtype: ExprType::Integer,
478                            },
479                            ArgSepSyntax::Exactly(ArgSep::Long),
480                        ),
481                        SingularArgSyntax::RequiredValue(
482                            RequiredValueSyntax {
483                                name: Cow::Borrowed("y2"),
484                                vtype: ExprType::Integer,
485                            },
486                            ArgSepSyntax::End,
487                        ),
488                    ],
489                    None,
490                )])
491                .with_category(CATEGORY)
492                .with_description(
493                    "Draws a filled rectangle from (x1,y1) to (x2,y2).
494The outline and area of the rectangle are drawn using the foreground color as selected by COLOR.",
495                )
496                .build(),
497            console,
498        })
499    }
500}
501
502#[async_trait(?Send)]
503impl Callable for GfxRectfCommand {
504    fn metadata(&self) -> &CallableMetadata {
505        &self.metadata
506    }
507
508    async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> {
509        debug_assert_eq!(4, scope.nargs());
510        let (x1value, x1pos) = scope.pop_integer_with_pos();
511        let (y1value, y1pos) = scope.pop_integer_with_pos();
512        let (x2value, x2pos) = scope.pop_integer_with_pos();
513        let (y2value, y2pos) = scope.pop_integer_with_pos();
514
515        let x1y1 = parse_coordinates(x1value, x1pos, y1value, y1pos)?;
516        let x2y2 = parse_coordinates(x2value, x2pos, y2value, y2pos)?;
517
518        self.console.borrow_mut().draw_rect_filled(x1y1, x2y2).map_err(|e| scope.io_error(e))?;
519        Ok(())
520    }
521}
522
523/// The `GFX_SYNC` command.
524pub struct GfxSyncCommand {
525    metadata: CallableMetadata,
526    console: Rc<RefCell<dyn Console>>,
527}
528
529impl GfxSyncCommand {
530    /// Creates a new `GFX_SYNC` command that controls video syncing on `console`.
531    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
532        Rc::from(Self {
533            metadata: CallableMetadataBuilder::new("GFX_SYNC")
534                .with_syntax(&[
535                    (&[], None),
536                    (
537                        &[SingularArgSyntax::RequiredValue(
538                            RequiredValueSyntax {
539                                name: Cow::Borrowed("enabled"),
540                                vtype: ExprType::Boolean,
541                            },
542                            ArgSepSyntax::End,
543                        )],
544                        None,
545                    ),
546                ])
547                .with_category(CATEGORY)
548                .with_description(
549                    "Controls the video syncing flag and/or forces a sync.
550With no arguments, this command triggers a video sync without updating the video syncing flag.  \
551When enabled? is specified, this updates the video syncing flag accordingly and triggers a video \
552sync if enabled? is TRUE.
553When video syncing is enabled, all console commands immediately refresh the console.  This is \
554useful to see the effects of the commands right away, which is why this is the default mode in the \
555interpreter.  However, this is a *very* inefficient way of drawing.
556When video syncing is disabled, all console updates are buffered until video syncing is enabled \
557again.  This is perfect to draw complex graphics efficiently.  If this is what you want to do, \
558you should disable syncing first, render a frame, call GFX_SYNC to flush the frame, repeat until \
559you are done, and then enable video syncing again.  Note that the textual cursor is not visible \
560when video syncing is disabled.
561WARNING: Be aware that if you disable video syncing in the interactive interpreter, you will not \
562be able to see what you are typing any longer until you reenable video syncing.",
563                )
564                .build(),
565            console,
566        })
567    }
568}
569
570#[async_trait(?Send)]
571impl Callable for GfxSyncCommand {
572    fn metadata(&self) -> &CallableMetadata {
573        &self.metadata
574    }
575
576    async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> {
577        if scope.nargs() == 0 {
578            self.console.borrow_mut().sync_now().map_err(|e| scope.io_error(e))?;
579            Ok(())
580        } else {
581            debug_assert_eq!(1, scope.nargs());
582            let enabled = scope.pop_boolean();
583
584            let mut console = self.console.borrow_mut();
585            if enabled {
586                console.show_cursor().map_err(|e| scope.io_error(e))?;
587            } else {
588                console.hide_cursor().map_err(|e| scope.io_error(e))?;
589            }
590            console.set_sync(enabled).map_err(|e| scope.io_error(e))?;
591            Ok(())
592        }
593    }
594}
595
596/// The `GFX_WIDTH` function.
597pub struct GfxWidthFunction {
598    metadata: CallableMetadata,
599    console: Rc<RefCell<dyn Console>>,
600}
601
602impl GfxWidthFunction {
603    /// Creates a new instance of the function.
604    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
605        Rc::from(Self {
606            metadata: CallableMetadataBuilder::new("GFX_WIDTH")
607                .with_return_type(ExprType::Integer)
608                .with_syntax(&[(&[], None)])
609                .with_category(CATEGORY)
610                .with_description(
611                    "Returns the width in pixels of the graphical console.
612See GFX_HEIGHT to query the other dimension.",
613                )
614                .build(),
615            console,
616        })
617    }
618}
619
620#[async_trait(?Send)]
621impl Callable for GfxWidthFunction {
622    fn metadata(&self) -> &CallableMetadata {
623        &self.metadata
624    }
625
626    async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> Result<()> {
627        debug_assert_eq!(0, scope.nargs());
628        let size = self.console.borrow().size_pixels().map_err(|e| scope.io_error(e))?;
629        scope.return_integer(i32::from(size.width))
630    }
631}
632
633/// Adds all console-related commands for the given `console` to the `machine`.
634pub fn add_all(machine: &mut Machine, console: Rc<RefCell<dyn Console>>) {
635    machine.add_callable(GfxCircleCommand::new(console.clone()));
636    machine.add_callable(GfxCirclefCommand::new(console.clone()));
637    machine.add_callable(GfxHeightFunction::new(console.clone()));
638    machine.add_callable(GfxLineCommand::new(console.clone()));
639    machine.add_callable(GfxPixelCommand::new(console.clone()));
640    machine.add_callable(GfxRectCommand::new(console.clone()));
641    machine.add_callable(GfxRectfCommand::new(console.clone()));
642    machine.add_callable(GfxSyncCommand::new(console.clone()));
643    machine.add_callable(GfxWidthFunction::new(console));
644}
645
646#[cfg(test)]
647mod tests {
648    use super::*;
649    use crate::console::SizeInPixels;
650    use crate::testutils::*;
651
652    /// Verifies error conditions for a command named `name` that takes to X/Y pairs.
653    fn check_errors_two_xy(name: &'static str) {
654        for args in &["1, 2, , 4", "1, 2, 3", "1, 2, 3, 4, 5", "2; 3, 4"] {
655            check_stmt_compilation_err(
656                format!("1:1: {} expected x1%, y1%, x2%, y2%", name),
657                &format!("{} {}", name, args),
658            );
659        }
660
661        for args in &["-40000, 1, 1, 1", "1, -40000, 1, 1", "1, 1, -40000, 1", "1, 1, 1, -40000"] {
662            let pos = name.len() + 1 + args.find('-').unwrap() + 1;
663            check_stmt_err(
664                format!("1:{}: Coordinate -40000 out of range", pos),
665                &format!("{} {}", name, args),
666            );
667        }
668
669        for args in &["40000, 1, 1, 1", "1, 40000, 1, 1", "1, 1, 40000, 1", "1, 1, 1, 40000"] {
670            let pos = name.len() + 1 + args.find('4').unwrap() + 1;
671            check_stmt_err(
672                format!("1:{}: Coordinate 40000 out of range", pos),
673                &format!("{} {}", name, args),
674            );
675        }
676
677        for args in &["\"a\", 1, 1, 1", "1, \"a\", 1, 1", "1, 1, \"a\", 1", "1, 1, 1, \"a\""] {
678            let stmt = &format!("{} {}", name, args);
679            let pos = stmt.find('"').unwrap() + 1;
680            check_stmt_compilation_err(format!("1:{}: STRING is not a number", pos), stmt);
681        }
682    }
683
684    /// Verifies error conditions for a command named `name` that takes an X/Y pair and a radius.
685    fn check_errors_xy_radius(name: &'static str) {
686        for args in &["1, , 3", "1, 2", "1, 2, 3, 4"] {
687            check_stmt_compilation_err(
688                format!("1:1: {} expected x%, y%, r%", name),
689                &format!("{} {}", name, args),
690            );
691        }
692        check_stmt_compilation_err(
693            format!("1:{}: {} expected x%, y%, r%", name.len() + 3, name),
694            &format!("{} 2; 3, 4", name),
695        );
696
697        for args in &["-40000, 1, 1", "1, -40000, 1"] {
698            let pos = name.len() + 1 + args.find('-').unwrap() + 1;
699            check_stmt_err(
700                format!("1:{}: Coordinate -40000 out of range", pos),
701                &format!("{} {}", name, args),
702            );
703        }
704        check_stmt_err(
705            format!("1:{}: Radius -40000 must be positive", name.len() + 8),
706            &format!("{} 1, 1, -40000", name),
707        );
708
709        for args in &["40000, 1, 1", "1, 40000, 1"] {
710            let pos = name.len() + 1 + args.find('4').unwrap() + 1;
711            check_stmt_err(
712                format!("1:{}: Coordinate 40000 out of range", pos),
713                &format!("{} {}", name, args),
714            );
715        }
716        check_stmt_err(
717            format!("1:{}: Radius 80000 out of range", name.len() + 8),
718            &format!("{} 1, 1, 80000", name),
719        );
720
721        for args in &["\"a\", 1, 1", "1, \"a\", 1", "1, 1, \"a\""] {
722            let stmt = &format!("{} {}", name, args);
723            let pos = stmt.find('"').unwrap() + 1;
724            check_stmt_compilation_err(format!("1:{}: STRING is not a number", pos), stmt);
725        }
726
727        check_stmt_err(
728            format!("1:{}: Radius -1 must be positive", name.len() + 8),
729            &format!("{} 1, 1, -1", name),
730        );
731    }
732
733    #[test]
734    fn test_gfx_circle_ok() {
735        Tester::default()
736            .run("GFX_CIRCLE 0, 0, 0")
737            .expect_output([CapturedOut::DrawCircle(PixelsXY { x: 0, y: 0 }, 0)])
738            .check();
739
740        Tester::default()
741            .run("GFX_CIRCLE 1.1, 2.3, 2.5")
742            .expect_output([CapturedOut::DrawCircle(PixelsXY { x: 1, y: 2 }, 3)])
743            .check();
744
745        Tester::default()
746            .run("GFX_CIRCLE -31000, -32000, 31000")
747            .expect_output([CapturedOut::DrawCircle(PixelsXY { x: -31000, y: -32000 }, 31000)])
748            .check();
749    }
750
751    #[test]
752    fn test_gfx_circle_errors() {
753        check_errors_xy_radius("GFX_CIRCLE");
754    }
755
756    #[test]
757    fn test_gfx_circlef_ok() {
758        Tester::default()
759            .run("GFX_CIRCLEF 0, 0, 0")
760            .expect_output([CapturedOut::DrawCircleFilled(PixelsXY { x: 0, y: 0 }, 0)])
761            .check();
762
763        Tester::default()
764            .run("GFX_CIRCLEF 1.1, 2.3, 2.5")
765            .expect_output([CapturedOut::DrawCircleFilled(PixelsXY { x: 1, y: 2 }, 3)])
766            .check();
767
768        Tester::default()
769            .run("GFX_CIRCLEF -31000, -32000, 31000")
770            .expect_output([CapturedOut::DrawCircleFilled(
771                PixelsXY { x: -31000, y: -32000 },
772                31000,
773            )])
774            .check();
775    }
776
777    #[test]
778    fn test_gfx_circlef_errors() {
779        check_errors_xy_radius("GFX_CIRCLEF");
780    }
781
782    #[test]
783    fn test_gfx_height() {
784        let mut t = Tester::default();
785        t.get_console().borrow_mut().set_size_pixels(SizeInPixels::new(1, 768));
786        t.run("result = GFX_HEIGHT").expect_var("result", 768i32).check();
787
788        check_expr_error("1:10: Graphical console size not yet set", "GFX_HEIGHT");
789
790        check_expr_compilation_error("1:10: GFX_HEIGHT expected no arguments", "GFX_HEIGHT()");
791        check_expr_compilation_error("1:10: GFX_HEIGHT expected no arguments", "GFX_HEIGHT(1)");
792    }
793
794    #[test]
795    fn test_gfx_line_ok() {
796        Tester::default()
797            .run("GFX_LINE 1, 2, 3, 4")
798            .expect_output([CapturedOut::DrawLine(
799                PixelsXY { x: 1, y: 2 },
800                PixelsXY { x: 3, y: 4 },
801            )])
802            .check();
803
804        Tester::default()
805            .run("GFX_LINE -31000.3, -32000.2, 31000.4, 31999.8")
806            .expect_output([CapturedOut::DrawLine(
807                PixelsXY { x: -31000, y: -32000 },
808                PixelsXY { x: 31000, y: 32000 },
809            )])
810            .check();
811    }
812
813    #[test]
814    fn test_gfx_line_errors() {
815        check_errors_two_xy("GFX_LINE");
816    }
817
818    #[test]
819    fn test_gfx_pixel_ok() {
820        Tester::default()
821            .run("GFX_PIXEL 1, 2")
822            .expect_output([CapturedOut::DrawPixel(PixelsXY { x: 1, y: 2 })])
823            .check();
824
825        Tester::default()
826            .run("GFX_PIXEL -31000, -32000")
827            .expect_output([CapturedOut::DrawPixel(PixelsXY { x: -31000, y: -32000 })])
828            .check();
829
830        Tester::default()
831            .run("GFX_PIXEL 30999.5, 31999.7")
832            .expect_output([CapturedOut::DrawPixel(PixelsXY { x: 31000, y: 32000 })])
833            .check();
834    }
835
836    #[test]
837    fn test_gfx_pixel_errors() {
838        for cmd in &["GFX_PIXEL , 2", "GFX_PIXEL 1, 2, 3", "GFX_PIXEL 1"] {
839            check_stmt_compilation_err("1:1: GFX_PIXEL expected x%, y%", cmd);
840        }
841        check_stmt_compilation_err("1:12: GFX_PIXEL expected x%, y%", "GFX_PIXEL 1; 2");
842
843        for cmd in &["GFX_PIXEL -40000, 1", "GFX_PIXEL 1, -40000"] {
844            check_stmt_err(
845                format!("1:{}: Coordinate -40000 out of range", cmd.find('-').unwrap() + 1),
846                cmd,
847            );
848        }
849
850        for cmd in &["GFX_PIXEL \"a\", 1", "GFX_PIXEL 1, \"a\""] {
851            let pos = cmd.find('"').unwrap() + 1;
852            check_stmt_compilation_err(format!("1:{}: STRING is not a number", pos), cmd);
853        }
854    }
855
856    #[test]
857    fn test_gfx_rect_ok() {
858        Tester::default()
859            .run("GFX_RECT 1.1, 2.3, 2.5, 3.9")
860            .expect_output([CapturedOut::DrawRect(
861                PixelsXY { x: 1, y: 2 },
862                PixelsXY { x: 3, y: 4 },
863            )])
864            .check();
865
866        Tester::default()
867            .run("GFX_RECT -31000, -32000, 31000, 32000")
868            .expect_output([CapturedOut::DrawRect(
869                PixelsXY { x: -31000, y: -32000 },
870                PixelsXY { x: 31000, y: 32000 },
871            )])
872            .check();
873    }
874
875    #[test]
876    fn test_gfx_rect_errors() {
877        check_errors_two_xy("GFX_RECT");
878    }
879
880    #[test]
881    fn test_gfx_rectf_ok() {
882        Tester::default()
883            .run("GFX_RECTF 1.1, 2.3, 2.5, 3.9")
884            .expect_output([CapturedOut::DrawRectFilled(
885                PixelsXY { x: 1, y: 2 },
886                PixelsXY { x: 3, y: 4 },
887            )])
888            .check();
889
890        Tester::default()
891            .run("GFX_RECTF -31000, -32000, 31000, 32000")
892            .expect_output([CapturedOut::DrawRectFilled(
893                PixelsXY { x: -31000, y: -32000 },
894                PixelsXY { x: 31000, y: 32000 },
895            )])
896            .check();
897    }
898
899    #[test]
900    fn test_gfx_rectf_errors() {
901        check_errors_two_xy("GFX_RECTF");
902    }
903
904    #[test]
905    fn test_gfx_sync_ok() {
906        Tester::default().run("GFX_SYNC").expect_output([CapturedOut::SyncNow]).check();
907        Tester::default()
908            .run("GFX_SYNC TRUE")
909            .expect_output([CapturedOut::ShowCursor, CapturedOut::SetSync(true)])
910            .check();
911        Tester::default()
912            .run("GFX_SYNC FALSE")
913            .expect_output([CapturedOut::HideCursor, CapturedOut::SetSync(false)])
914            .check();
915    }
916
917    #[test]
918    fn test_gfx_sync_errors() {
919        check_stmt_compilation_err("1:1: GFX_SYNC expected <> | <enabled?>", "GFX_SYNC 2, 3");
920        check_stmt_compilation_err("1:10: Expected BOOLEAN but found INTEGER", "GFX_SYNC 2");
921    }
922
923    #[test]
924    fn test_gfx_width() {
925        let mut t = Tester::default();
926        t.get_console().borrow_mut().set_size_pixels(SizeInPixels::new(12345, 1));
927        t.run("result = GFX_WIDTH").expect_var("result", 12345i32).check();
928
929        check_expr_error("1:10: Graphical console size not yet set", "GFX_WIDTH");
930
931        check_expr_compilation_error("1:10: GFX_WIDTH expected no arguments", "GFX_WIDTH()");
932        check_expr_compilation_error("1:10: GFX_WIDTH expected no arguments", "GFX_WIDTH(1)");
933    }
934}