Skip to main content

endbasic_std/gfx/
mod.rs

1// EndBASIC
2// Copyright 2021 Julio Merino
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Commands for graphical console interaction.
18
19use crate::console::{Console, PixelsXY};
20use endbasic_core::{
21    ArgSep, ArgSepSyntax, CallError, CallResult, Callable, CallableMetadata,
22    CallableMetadataBuilder, ExprType, LineCol, RepeatedSyntax, RepeatedTypeSyntax,
23    RequiredRefSyntax, RequiredValueSyntax, Scope, SingularArgSyntax,
24};
25use std::borrow::Cow;
26use std::cell::RefCell;
27use std::convert::TryFrom;
28use std::rc::Rc;
29
30use crate::MachineBuilder;
31
32pub mod lcd;
33
34/// Category description for all symbols provided by this module.
35const CATEGORY: &str = "Graphics
36The EndBASIC console overlays text and graphics in the same canvas.  The consequence of this \
37design choice is that the console has two coordinate systems: the character-based system, used by
38the commands described in HELP \"CONSOLE\", and the pixel-based system, used by the commands \
39described in this section.";
40
41/// Parses an expression that represents a single coordinate.
42fn parse_coordinate(scope: &Scope<'_>, narg: u8) -> CallResult<i16> {
43    parse_coordinate_value(scope.get_pos(narg), scope.get_integer(narg))
44}
45
46/// Parses an integer that represents a single coordinate.
47fn parse_coordinate_value(pos: LineCol, i: i32) -> CallResult<i16> {
48    match i16::try_from(i) {
49        Ok(i) => Ok(i),
50        Err(_) => Err(CallError::Syntax(pos, format!("Coordinate {} out of range", i))),
51    }
52}
53
54/// Parses a pair of expressions that represent an (x,y) coordinate pair.
55fn parse_coordinates(scope: &Scope<'_>, xarg: u8, yarg: u8) -> CallResult<PixelsXY> {
56    Ok(PixelsXY { x: parse_coordinate(scope, xarg)?, y: parse_coordinate(scope, yarg)? })
57}
58
59/// Parses three pairs of expressions that represent triangle vertex coordinates.
60fn parse_triangle_coordinates(
61    scope: &Scope<'_>,
62    x1arg: u8,
63    y1arg: u8,
64    x2arg: u8,
65    y2arg: u8,
66    x3arg: u8,
67    y3arg: u8,
68) -> CallResult<(PixelsXY, PixelsXY, PixelsXY)> {
69    Ok((
70        parse_coordinates(scope, x1arg, y1arg)?,
71        parse_coordinates(scope, x2arg, y2arg)?,
72        parse_coordinates(scope, x3arg, y3arg)?,
73    ))
74}
75
76/// Parses a variable-length sequence of X/Y coordinate pairs.
77fn parse_polygon_coordinates(scope: &Scope<'_>) -> CallResult<Vec<PixelsXY>> {
78    match scope.nargs() {
79        0 => return Ok(vec![]),
80        1 => return parse_polygon_coordinates_from_array(scope),
81        _ => (),
82    }
83
84    if !scope.nargs().is_multiple_of(2) {
85        let narg = u8::try_from(scope.nargs() - 1).expect("Argument index must fit in u8");
86        return Err(CallError::Syntax(
87            scope.get_pos(narg),
88            "Expected an even number of coordinates".to_owned(),
89        ));
90    }
91
92    let mut points = Vec::with_capacity(scope.nargs() / 2);
93    for i in (0..scope.nargs()).step_by(2) {
94        let xarg = u8::try_from(i).expect("Argument index must fit in u8");
95        let yarg = u8::try_from(i + 1).expect("Argument index must fit in u8");
96        points.push(parse_coordinates(scope, xarg, yarg)?);
97    }
98    Ok(points)
99}
100
101/// Parses an array reference that contains a variable-length sequence of X/Y coordinate pairs.
102fn parse_polygon_coordinates_from_array(scope: &Scope<'_>) -> CallResult<Vec<PixelsXY>> {
103    debug_assert_eq!(1, scope.nargs());
104
105    let array = scope.get_ref(0);
106    let pos = scope.get_pos(0);
107    if array.vtype != ExprType::Integer {
108        return Err(CallError::Syntax(pos, "Expected an INTEGER array".to_owned()));
109    }
110
111    let dimensions = array.array_dimensions();
112    match dimensions {
113        [ncoords] => {
114            if !ncoords.is_multiple_of(2) {
115                return Err(CallError::Syntax(
116                    pos,
117                    "Expected an even number of coordinates".to_owned(),
118                ));
119            }
120
121            let mut points = Vec::with_capacity(*ncoords / 2);
122            for i in (0..*ncoords).step_by(2) {
123                let x = array
124                    .deref_array_integer(&[i as i32])
125                    .map_err(|e| CallError::Syntax(pos, e))?;
126                let y = array
127                    .deref_array_integer(&[(i + 1) as i32])
128                    .map_err(|e| CallError::Syntax(pos, e))?;
129                points.push(PixelsXY {
130                    x: parse_coordinate_value(pos, x)?,
131                    y: parse_coordinate_value(pos, y)?,
132                });
133            }
134            Ok(points)
135        }
136
137        [npoints, 2] => {
138            let mut points = Vec::with_capacity(*npoints);
139            for i in 0..*npoints {
140                let x = array
141                    .deref_array_integer(&[i as i32, 0])
142                    .map_err(|e| CallError::Syntax(pos, e))?;
143                let y = array
144                    .deref_array_integer(&[i as i32, 1])
145                    .map_err(|e| CallError::Syntax(pos, e))?;
146                points.push(PixelsXY {
147                    x: parse_coordinate_value(pos, x)?,
148                    y: parse_coordinate_value(pos, y)?,
149                });
150            }
151            Ok(points)
152        }
153
154        _ => Err(CallError::Syntax(
155            pos,
156            "Expected a flat array of coordinates or an Nx2 array of points".to_owned(),
157        )),
158    }
159}
160
161/// Parses an expression that represents a radius.
162fn parse_radius(scope: &Scope<'_>, narg: u8) -> CallResult<u16> {
163    let i = scope.get_integer(narg);
164    match u16::try_from(i) {
165        Ok(i) => Ok(i),
166        Err(_) if i < 0 => {
167            Err(CallError::Syntax(scope.get_pos(narg), format!("Radius {} must be positive", i)))
168        }
169        Err(_) => Err(CallError::Syntax(scope.get_pos(narg), format!("Radius {} out of range", i))),
170    }
171}
172
173/// The `GFX_CIRCLE` command.
174pub struct GfxCircleCommand {
175    metadata: Rc<CallableMetadata>,
176    console: Rc<RefCell<dyn Console>>,
177}
178
179impl GfxCircleCommand {
180    /// Creates a new `GFX_CIRCLE` command that draws an empty circle on `console`.
181    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
182        Rc::from(Self {
183            metadata: CallableMetadataBuilder::new("GFX_CIRCLE")
184                .with_syntax(&[(
185                    &[
186                        SingularArgSyntax::RequiredValue(
187                            RequiredValueSyntax {
188                                name: Cow::Borrowed("x"),
189                                vtype: ExprType::Integer,
190                            },
191                            ArgSepSyntax::Exactly(ArgSep::Long),
192                        ),
193                        SingularArgSyntax::RequiredValue(
194                            RequiredValueSyntax {
195                                name: Cow::Borrowed("y"),
196                                vtype: ExprType::Integer,
197                            },
198                            ArgSepSyntax::Exactly(ArgSep::Long),
199                        ),
200                        SingularArgSyntax::RequiredValue(
201                            RequiredValueSyntax {
202                                name: Cow::Borrowed("r"),
203                                vtype: ExprType::Integer,
204                            },
205                            ArgSepSyntax::End,
206                        ),
207                    ],
208                    None,
209                )])
210                .with_category(CATEGORY)
211                .with_description(
212                    "Draws a circle of radius r centered at (x,y).
213The outline of the circle is drawn using the foreground color as selected by COLOR and the \
214area of the circle is left untouched.",
215                )
216                .build(),
217            console,
218        })
219    }
220}
221
222impl Callable for GfxCircleCommand {
223    fn metadata(&self) -> Rc<CallableMetadata> {
224        self.metadata.clone()
225    }
226
227    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
228        debug_assert_eq!(3, scope.nargs());
229        let xy = parse_coordinates(&scope, 0, 1)?;
230        let r = parse_radius(&scope, 2)?;
231
232        self.console.borrow_mut().draw_circle(xy, r)?;
233        Ok(())
234    }
235}
236
237/// The `GFX_CIRCLEF` command.
238pub struct GfxCirclefCommand {
239    metadata: Rc<CallableMetadata>,
240    console: Rc<RefCell<dyn Console>>,
241}
242
243impl GfxCirclefCommand {
244    /// Creates a new `GFX_CIRCLEF` command that draws a filled circle on `console`.
245    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
246        Rc::from(Self {
247            metadata: CallableMetadataBuilder::new("GFX_CIRCLEF")
248                .with_syntax(&[(
249                    &[
250                        SingularArgSyntax::RequiredValue(
251                            RequiredValueSyntax {
252                                name: Cow::Borrowed("x"),
253                                vtype: ExprType::Integer,
254                            },
255                            ArgSepSyntax::Exactly(ArgSep::Long),
256                        ),
257                        SingularArgSyntax::RequiredValue(
258                            RequiredValueSyntax {
259                                name: Cow::Borrowed("y"),
260                                vtype: ExprType::Integer,
261                            },
262                            ArgSepSyntax::Exactly(ArgSep::Long),
263                        ),
264                        SingularArgSyntax::RequiredValue(
265                            RequiredValueSyntax {
266                                name: Cow::Borrowed("r"),
267                                vtype: ExprType::Integer,
268                            },
269                            ArgSepSyntax::End,
270                        ),
271                    ],
272                    None,
273                )])
274                .with_category(CATEGORY)
275                .with_description(
276                    "Draws a filled circle of radius r centered at (x,y).
277The outline and area of the circle are drawn using the foreground color as selected by COLOR.",
278                )
279                .build(),
280            console,
281        })
282    }
283}
284
285impl Callable for GfxCirclefCommand {
286    fn metadata(&self) -> Rc<CallableMetadata> {
287        self.metadata.clone()
288    }
289
290    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
291        debug_assert_eq!(3, scope.nargs());
292        let xy = parse_coordinates(&scope, 0, 1)?;
293        let r = parse_radius(&scope, 2)?;
294
295        self.console.borrow_mut().draw_circle_filled(xy, r)?;
296        Ok(())
297    }
298}
299
300/// The `GFX_FILL` command.
301pub struct GfxFillCommand {
302    metadata: Rc<CallableMetadata>,
303    console: Rc<RefCell<dyn Console>>,
304}
305
306impl GfxFillCommand {
307    /// Creates a new `GFX_FILL` command that fills a region on `console`.
308    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
309        Rc::from(Self {
310            metadata: CallableMetadataBuilder::new("GFX_FILL")
311                .with_syntax(&[(
312                    &[
313                        SingularArgSyntax::RequiredValue(
314                            RequiredValueSyntax {
315                                name: Cow::Borrowed("x"),
316                                vtype: ExprType::Integer,
317                            },
318                            ArgSepSyntax::Exactly(ArgSep::Long),
319                        ),
320                        SingularArgSyntax::RequiredValue(
321                            RequiredValueSyntax {
322                                name: Cow::Borrowed("y"),
323                                vtype: ExprType::Integer,
324                            },
325                            ArgSepSyntax::End,
326                        ),
327                    ],
328                    None,
329                )])
330                .with_category(CATEGORY)
331                .with_description(
332                    "Fills the region around (x,y).
333Fills the 4-connected region containing (x,y) using the foreground color as selected by COLOR.  \
334If the seed pixel lies outside of the graphical console or if its color cannot be mapped back to a \
335COLOR value, this command does nothing.",
336                )
337                .build(),
338            console,
339        })
340    }
341}
342
343impl Callable for GfxFillCommand {
344    fn metadata(&self) -> Rc<CallableMetadata> {
345        self.metadata.clone()
346    }
347
348    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
349        debug_assert_eq!(2, scope.nargs());
350        let xy = parse_coordinates(&scope, 0, 1)?;
351
352        self.console.borrow_mut().bucket_fill(xy)?;
353        Ok(())
354    }
355}
356
357/// The `GFX_HEIGHT` function.
358pub struct GfxHeightFunction {
359    metadata: Rc<CallableMetadata>,
360    console: Rc<RefCell<dyn Console>>,
361}
362
363impl GfxHeightFunction {
364    /// Creates a new instance of the function.
365    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
366        Rc::from(Self {
367            metadata: CallableMetadataBuilder::new("GFX_HEIGHT")
368                .with_return_type(ExprType::Integer)
369                .with_syntax(&[(&[], None)])
370                .with_category(CATEGORY)
371                .with_description(
372                    "Returns the height in pixels of the graphical console.
373See GFX_WIDTH to query the other dimension.",
374                )
375                .build(),
376            console,
377        })
378    }
379}
380
381impl Callable for GfxHeightFunction {
382    fn metadata(&self) -> Rc<CallableMetadata> {
383        self.metadata.clone()
384    }
385
386    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
387        debug_assert_eq!(0, scope.nargs());
388        let size = self.console.borrow().size_pixels()?;
389        scope.return_integer(i32::from(size.height))
390    }
391}
392
393/// The `GFX_LINE` command.
394pub struct GfxLineCommand {
395    metadata: Rc<CallableMetadata>,
396    console: Rc<RefCell<dyn Console>>,
397}
398
399impl GfxLineCommand {
400    /// Creates a new `GFX_LINE` command that draws a line on `console`.
401    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
402        Rc::from(Self {
403            metadata: CallableMetadataBuilder::new("GFX_LINE")
404                .with_syntax(&[(
405                    &[
406                        SingularArgSyntax::RequiredValue(
407                            RequiredValueSyntax {
408                                name: Cow::Borrowed("x1"),
409                                vtype: ExprType::Integer,
410                            },
411                            ArgSepSyntax::Exactly(ArgSep::Long),
412                        ),
413                        SingularArgSyntax::RequiredValue(
414                            RequiredValueSyntax {
415                                name: Cow::Borrowed("y1"),
416                                vtype: ExprType::Integer,
417                            },
418                            ArgSepSyntax::Exactly(ArgSep::Long),
419                        ),
420                        SingularArgSyntax::RequiredValue(
421                            RequiredValueSyntax {
422                                name: Cow::Borrowed("x2"),
423                                vtype: ExprType::Integer,
424                            },
425                            ArgSepSyntax::Exactly(ArgSep::Long),
426                        ),
427                        SingularArgSyntax::RequiredValue(
428                            RequiredValueSyntax {
429                                name: Cow::Borrowed("y2"),
430                                vtype: ExprType::Integer,
431                            },
432                            ArgSepSyntax::End,
433                        ),
434                    ],
435                    None,
436                )])
437                .with_category(CATEGORY)
438                .with_description(
439                    "Draws a line from (x1,y1) to (x2,y2).
440The line is drawn using the foreground color as selected by COLOR.",
441                )
442                .build(),
443            console,
444        })
445    }
446}
447
448impl Callable for GfxLineCommand {
449    fn metadata(&self) -> Rc<CallableMetadata> {
450        self.metadata.clone()
451    }
452
453    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
454        debug_assert_eq!(4, scope.nargs());
455        let x1y1 = parse_coordinates(&scope, 0, 1)?;
456        let x2y2 = parse_coordinates(&scope, 2, 3)?;
457
458        self.console.borrow_mut().draw_line(x1y1, x2y2)?;
459        Ok(())
460    }
461}
462
463/// The `GFX_PEEK` function.
464pub struct GfxPeekFunction {
465    metadata: Rc<CallableMetadata>,
466    console: Rc<RefCell<dyn Console>>,
467}
468
469impl GfxPeekFunction {
470    /// Creates a new instance of the function.
471    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
472        Rc::from(Self {
473            metadata: CallableMetadataBuilder::new("GFX_PEEK")
474                .with_return_type(ExprType::Integer)
475                .with_syntax(&[(
476                    &[
477                        SingularArgSyntax::RequiredValue(
478                            RequiredValueSyntax {
479                                name: Cow::Borrowed("x"),
480                                vtype: ExprType::Integer,
481                            },
482                            ArgSepSyntax::Exactly(ArgSep::Long),
483                        ),
484                        SingularArgSyntax::RequiredValue(
485                            RequiredValueSyntax {
486                                name: Cow::Borrowed("y"),
487                                vtype: ExprType::Integer,
488                            },
489                            ArgSepSyntax::End,
490                        ),
491                    ],
492                    None,
493                )])
494                .with_category(CATEGORY)
495                .with_description(
496                    "Returns the color number of the pixel at (x,y).
497Returns -1 if the pixel lies outside of the graphical console or if its color cannot be mapped \
498back to a COLOR value.",
499                )
500                .build(),
501            console,
502        })
503    }
504}
505
506impl Callable for GfxPeekFunction {
507    fn metadata(&self) -> Rc<CallableMetadata> {
508        self.metadata.clone()
509    }
510
511    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
512        debug_assert_eq!(2, scope.nargs());
513        let xy = parse_coordinates(&scope, 0, 1)?;
514        let color = self.console.borrow().peek_pixel(xy)?.map(i32::from).unwrap_or(-1);
515        scope.return_integer(color)
516    }
517}
518
519/// The `GFX_PIXEL` command.
520pub struct GfxPixelCommand {
521    metadata: Rc<CallableMetadata>,
522    console: Rc<RefCell<dyn Console>>,
523}
524
525impl GfxPixelCommand {
526    /// Creates a new `GFX_PIXEL` command that draws a single pixel on `console`.
527    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
528        Rc::from(Self {
529            metadata: CallableMetadataBuilder::new("GFX_PIXEL")
530                .with_syntax(&[(
531                    &[
532                        SingularArgSyntax::RequiredValue(
533                            RequiredValueSyntax {
534                                name: Cow::Borrowed("x"),
535                                vtype: ExprType::Integer,
536                            },
537                            ArgSepSyntax::Exactly(ArgSep::Long),
538                        ),
539                        SingularArgSyntax::RequiredValue(
540                            RequiredValueSyntax {
541                                name: Cow::Borrowed("y"),
542                                vtype: ExprType::Integer,
543                            },
544                            ArgSepSyntax::End,
545                        ),
546                    ],
547                    None,
548                )])
549                .with_category(CATEGORY)
550                .with_description(
551                    "Draws a pixel at (x,y).
552The pixel is drawn using the foreground color as selected by COLOR.",
553                )
554                .build(),
555            console,
556        })
557    }
558}
559
560impl Callable for GfxPixelCommand {
561    fn metadata(&self) -> Rc<CallableMetadata> {
562        self.metadata.clone()
563    }
564
565    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
566        debug_assert_eq!(2, scope.nargs());
567        let xy = parse_coordinates(&scope, 0, 1)?;
568
569        self.console.borrow_mut().draw_pixel(xy)?;
570        Ok(())
571    }
572}
573
574/// The `GFX_POLY` command.
575pub struct GfxPolyCommand {
576    metadata: Rc<CallableMetadata>,
577    console: Rc<RefCell<dyn Console>>,
578}
579
580impl GfxPolyCommand {
581    /// Creates a new `GFX_POLY` command that draws an outlined polygon on `console`.
582    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
583        Rc::from(Self {
584            metadata: CallableMetadataBuilder::new("GFX_POLY")
585                .with_syntax(&[
586                    (&[], None),
587                    (
588                        &[SingularArgSyntax::RequiredRef(
589                            RequiredRefSyntax {
590                                name: Cow::Borrowed("points"),
591                                require_array: true,
592                                define_undefined: false,
593                            },
594                            ArgSepSyntax::End,
595                        )],
596                        None,
597                    ),
598                    (
599                        &[
600                            SingularArgSyntax::RequiredValue(
601                                RequiredValueSyntax {
602                                    name: Cow::Borrowed("x1"),
603                                    vtype: ExprType::Integer,
604                                },
605                                ArgSepSyntax::Exactly(ArgSep::Long),
606                            ),
607                            SingularArgSyntax::RequiredValue(
608                                RequiredValueSyntax {
609                                    name: Cow::Borrowed("y1"),
610                                    vtype: ExprType::Integer,
611                                },
612                                ArgSepSyntax::Exactly(ArgSep::Long),
613                            ),
614                        ],
615                        Some(&RepeatedSyntax {
616                            name: Cow::Borrowed("coord"),
617                            type_syn: RepeatedTypeSyntax::TypedValue(ExprType::Integer),
618                            sep: ArgSepSyntax::Exactly(ArgSep::Long),
619                            require_one: false,
620                            allow_missing: false,
621                        }),
622                    ),
623                ])
624                .with_category(CATEGORY)
625                .with_description(
626                    "Draws a polygon with vertices at (x1,y1), (x2,y2), ..., and (xN,yN).
627The points can be specified either as individual coordinates or via an INTEGER array containing a \
628flat list of coordinates or an Nx2 matrix of points.
629The outline of the polygon is drawn using the foreground color as selected by COLOR and the area \
630of the polygon is left untouched.",
631                )
632                .build(),
633            console,
634        })
635    }
636}
637
638impl Callable for GfxPolyCommand {
639    fn metadata(&self) -> Rc<CallableMetadata> {
640        self.metadata.clone()
641    }
642
643    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
644        let points = parse_polygon_coordinates(&scope)?;
645
646        self.console.borrow_mut().draw_poly(&points)?;
647        Ok(())
648    }
649}
650
651/// The `GFX_POLYF` command.
652pub struct GfxPolyfCommand {
653    metadata: Rc<CallableMetadata>,
654    console: Rc<RefCell<dyn Console>>,
655}
656
657impl GfxPolyfCommand {
658    /// Creates a new `GFX_POLYF` command that draws a filled polygon on `console`.
659    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
660        Rc::from(Self {
661            metadata: CallableMetadataBuilder::new("GFX_POLYF")
662                .with_syntax(&[
663                    (&[], None),
664                    (
665                        &[SingularArgSyntax::RequiredRef(
666                            RequiredRefSyntax {
667                                name: Cow::Borrowed("points"),
668                                require_array: true,
669                                define_undefined: false,
670                            },
671                            ArgSepSyntax::End,
672                        )],
673                        None,
674                    ),
675                    (
676                        &[
677                            SingularArgSyntax::RequiredValue(
678                                RequiredValueSyntax {
679                                    name: Cow::Borrowed("x1"),
680                                    vtype: ExprType::Integer,
681                                },
682                                ArgSepSyntax::Exactly(ArgSep::Long),
683                            ),
684                            SingularArgSyntax::RequiredValue(
685                                RequiredValueSyntax {
686                                    name: Cow::Borrowed("y1"),
687                                    vtype: ExprType::Integer,
688                                },
689                                ArgSepSyntax::Exactly(ArgSep::Long),
690                            ),
691                        ],
692                        Some(&RepeatedSyntax {
693                            name: Cow::Borrowed("coord"),
694                            type_syn: RepeatedTypeSyntax::TypedValue(ExprType::Integer),
695                            sep: ArgSepSyntax::Exactly(ArgSep::Long),
696                            require_one: false,
697                            allow_missing: false,
698                        }),
699                    ),
700                ])
701                .with_category(CATEGORY)
702                .with_description(
703                    "Draws a filled polygon with vertices at (x1,y1), (x2,y2), ..., and (xN,yN).
704The points can be specified either as individual coordinates or via an INTEGER array containing a \
705flat list of coordinates or an Nx2 matrix of points.
706The outline and area of the polygon are drawn using the foreground color as selected by COLOR.",
707                )
708                .build(),
709            console,
710        })
711    }
712}
713
714impl Callable for GfxPolyfCommand {
715    fn metadata(&self) -> Rc<CallableMetadata> {
716        self.metadata.clone()
717    }
718
719    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
720        let points = parse_polygon_coordinates(&scope)?;
721
722        self.console.borrow_mut().draw_poly_filled(&points)?;
723        Ok(())
724    }
725}
726
727/// The `GFX_RECT` command.
728pub struct GfxRectCommand {
729    metadata: Rc<CallableMetadata>,
730    console: Rc<RefCell<dyn Console>>,
731}
732
733impl GfxRectCommand {
734    /// Creates a new `GFX_RECT` command that draws an empty rectangle on `console`.
735    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
736        Rc::from(Self {
737            metadata: CallableMetadataBuilder::new("GFX_RECT")
738                .with_syntax(&[(
739                    &[
740                        SingularArgSyntax::RequiredValue(
741                            RequiredValueSyntax {
742                                name: Cow::Borrowed("x1"),
743                                vtype: ExprType::Integer,
744                            },
745                            ArgSepSyntax::Exactly(ArgSep::Long),
746                        ),
747                        SingularArgSyntax::RequiredValue(
748                            RequiredValueSyntax {
749                                name: Cow::Borrowed("y1"),
750                                vtype: ExprType::Integer,
751                            },
752                            ArgSepSyntax::Exactly(ArgSep::Long),
753                        ),
754                        SingularArgSyntax::RequiredValue(
755                            RequiredValueSyntax {
756                                name: Cow::Borrowed("x2"),
757                                vtype: ExprType::Integer,
758                            },
759                            ArgSepSyntax::Exactly(ArgSep::Long),
760                        ),
761                        SingularArgSyntax::RequiredValue(
762                            RequiredValueSyntax {
763                                name: Cow::Borrowed("y2"),
764                                vtype: ExprType::Integer,
765                            },
766                            ArgSepSyntax::End,
767                        ),
768                    ],
769                    None,
770                )])
771                .with_category(CATEGORY)
772                .with_description(
773                    "Draws a rectangle from (x1,y1) to (x2,y2).
774The outline of the rectangle is drawn using the foreground color as selected by COLOR and the \
775area of the rectangle is left untouched.",
776                )
777                .build(),
778            console,
779        })
780    }
781}
782
783impl Callable for GfxRectCommand {
784    fn metadata(&self) -> Rc<CallableMetadata> {
785        self.metadata.clone()
786    }
787
788    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
789        debug_assert_eq!(4, scope.nargs());
790        let x1y1 = parse_coordinates(&scope, 0, 1)?;
791        let x2y2 = parse_coordinates(&scope, 2, 3)?;
792
793        self.console.borrow_mut().draw_rect(x1y1, x2y2)?;
794        Ok(())
795    }
796}
797
798/// The `GFX_RECTF` command.
799pub struct GfxRectfCommand {
800    metadata: Rc<CallableMetadata>,
801    console: Rc<RefCell<dyn Console>>,
802}
803
804impl GfxRectfCommand {
805    /// Creates a new `GFX_RECTF` command that draws a filled rectangle on `console`.
806    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
807        Rc::from(Self {
808            metadata: CallableMetadataBuilder::new("GFX_RECTF")
809                .with_syntax(&[(
810                    &[
811                        SingularArgSyntax::RequiredValue(
812                            RequiredValueSyntax {
813                                name: Cow::Borrowed("x1"),
814                                vtype: ExprType::Integer,
815                            },
816                            ArgSepSyntax::Exactly(ArgSep::Long),
817                        ),
818                        SingularArgSyntax::RequiredValue(
819                            RequiredValueSyntax {
820                                name: Cow::Borrowed("y1"),
821                                vtype: ExprType::Integer,
822                            },
823                            ArgSepSyntax::Exactly(ArgSep::Long),
824                        ),
825                        SingularArgSyntax::RequiredValue(
826                            RequiredValueSyntax {
827                                name: Cow::Borrowed("x2"),
828                                vtype: ExprType::Integer,
829                            },
830                            ArgSepSyntax::Exactly(ArgSep::Long),
831                        ),
832                        SingularArgSyntax::RequiredValue(
833                            RequiredValueSyntax {
834                                name: Cow::Borrowed("y2"),
835                                vtype: ExprType::Integer,
836                            },
837                            ArgSepSyntax::End,
838                        ),
839                    ],
840                    None,
841                )])
842                .with_category(CATEGORY)
843                .with_description(
844                    "Draws a filled rectangle from (x1,y1) to (x2,y2).
845The outline and area of the rectangle are drawn using the foreground color as selected by COLOR.",
846                )
847                .build(),
848            console,
849        })
850    }
851}
852
853impl Callable for GfxRectfCommand {
854    fn metadata(&self) -> Rc<CallableMetadata> {
855        self.metadata.clone()
856    }
857
858    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
859        debug_assert_eq!(4, scope.nargs());
860        let x1y1 = parse_coordinates(&scope, 0, 1)?;
861        let x2y2 = parse_coordinates(&scope, 2, 3)?;
862
863        self.console.borrow_mut().draw_rect_filled(x1y1, x2y2)?;
864        Ok(())
865    }
866}
867
868/// The `GFX_SYNC` command.
869pub struct GfxSyncCommand {
870    metadata: Rc<CallableMetadata>,
871    console: Rc<RefCell<dyn Console>>,
872}
873
874/// The `GFX_TRI` command.
875pub struct GfxTriCommand {
876    metadata: Rc<CallableMetadata>,
877    console: Rc<RefCell<dyn Console>>,
878}
879
880impl GfxTriCommand {
881    /// Creates a new `GFX_TRI` command that draws an empty triangle on `console`.
882    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
883        Rc::from(Self {
884            metadata: CallableMetadataBuilder::new("GFX_TRI")
885                .with_syntax(&[(
886                    &[
887                        SingularArgSyntax::RequiredValue(
888                            RequiredValueSyntax {
889                                name: Cow::Borrowed("x1"),
890                                vtype: ExprType::Integer,
891                            },
892                            ArgSepSyntax::Exactly(ArgSep::Long),
893                        ),
894                        SingularArgSyntax::RequiredValue(
895                            RequiredValueSyntax {
896                                name: Cow::Borrowed("y1"),
897                                vtype: ExprType::Integer,
898                            },
899                            ArgSepSyntax::Exactly(ArgSep::Long),
900                        ),
901                        SingularArgSyntax::RequiredValue(
902                            RequiredValueSyntax {
903                                name: Cow::Borrowed("x2"),
904                                vtype: ExprType::Integer,
905                            },
906                            ArgSepSyntax::Exactly(ArgSep::Long),
907                        ),
908                        SingularArgSyntax::RequiredValue(
909                            RequiredValueSyntax {
910                                name: Cow::Borrowed("y2"),
911                                vtype: ExprType::Integer,
912                            },
913                            ArgSepSyntax::Exactly(ArgSep::Long),
914                        ),
915                        SingularArgSyntax::RequiredValue(
916                            RequiredValueSyntax {
917                                name: Cow::Borrowed("x3"),
918                                vtype: ExprType::Integer,
919                            },
920                            ArgSepSyntax::Exactly(ArgSep::Long),
921                        ),
922                        SingularArgSyntax::RequiredValue(
923                            RequiredValueSyntax {
924                                name: Cow::Borrowed("y3"),
925                                vtype: ExprType::Integer,
926                            },
927                            ArgSepSyntax::End,
928                        ),
929                    ],
930                    None,
931                )])
932                .with_category(CATEGORY)
933                .with_description(
934                    "Draws a triangle with vertices at (x1,y1), (x2,y2), and (x3,y3).
935The outline of the triangle is drawn using the foreground color as selected by COLOR and the \
936area of the triangle is left untouched.",
937                )
938                .build(),
939            console,
940        })
941    }
942}
943
944impl Callable for GfxTriCommand {
945    fn metadata(&self) -> Rc<CallableMetadata> {
946        self.metadata.clone()
947    }
948
949    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
950        debug_assert_eq!(6, scope.nargs());
951        let (x1y1, x2y2, x3y3) = parse_triangle_coordinates(&scope, 0, 1, 2, 3, 4, 5)?;
952
953        self.console.borrow_mut().draw_tri(x1y1, x2y2, x3y3)?;
954        Ok(())
955    }
956}
957
958/// The `GFX_TRIF` command.
959pub struct GfxTrifCommand {
960    metadata: Rc<CallableMetadata>,
961    console: Rc<RefCell<dyn Console>>,
962}
963
964impl GfxTrifCommand {
965    /// Creates a new `GFX_TRIF` command that draws a filled triangle on `console`.
966    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
967        Rc::from(Self {
968            metadata: CallableMetadataBuilder::new("GFX_TRIF")
969                .with_syntax(&[(
970                    &[
971                        SingularArgSyntax::RequiredValue(
972                            RequiredValueSyntax {
973                                name: Cow::Borrowed("x1"),
974                                vtype: ExprType::Integer,
975                            },
976                            ArgSepSyntax::Exactly(ArgSep::Long),
977                        ),
978                        SingularArgSyntax::RequiredValue(
979                            RequiredValueSyntax {
980                                name: Cow::Borrowed("y1"),
981                                vtype: ExprType::Integer,
982                            },
983                            ArgSepSyntax::Exactly(ArgSep::Long),
984                        ),
985                        SingularArgSyntax::RequiredValue(
986                            RequiredValueSyntax {
987                                name: Cow::Borrowed("x2"),
988                                vtype: ExprType::Integer,
989                            },
990                            ArgSepSyntax::Exactly(ArgSep::Long),
991                        ),
992                        SingularArgSyntax::RequiredValue(
993                            RequiredValueSyntax {
994                                name: Cow::Borrowed("y2"),
995                                vtype: ExprType::Integer,
996                            },
997                            ArgSepSyntax::Exactly(ArgSep::Long),
998                        ),
999                        SingularArgSyntax::RequiredValue(
1000                            RequiredValueSyntax {
1001                                name: Cow::Borrowed("x3"),
1002                                vtype: ExprType::Integer,
1003                            },
1004                            ArgSepSyntax::Exactly(ArgSep::Long),
1005                        ),
1006                        SingularArgSyntax::RequiredValue(
1007                            RequiredValueSyntax {
1008                                name: Cow::Borrowed("y3"),
1009                                vtype: ExprType::Integer,
1010                            },
1011                            ArgSepSyntax::End,
1012                        ),
1013                    ],
1014                    None,
1015                )])
1016                .with_category(CATEGORY)
1017                .with_description(
1018                    "Draws a filled triangle with vertices at (x1,y1), (x2,y2), and (x3,y3).
1019The outline and area of the triangle are drawn using the foreground color as selected by COLOR.",
1020                )
1021                .build(),
1022            console,
1023        })
1024    }
1025}
1026
1027impl Callable for GfxTrifCommand {
1028    fn metadata(&self) -> Rc<CallableMetadata> {
1029        self.metadata.clone()
1030    }
1031
1032    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
1033        debug_assert_eq!(6, scope.nargs());
1034        let (x1y1, x2y2, x3y3) = parse_triangle_coordinates(&scope, 0, 1, 2, 3, 4, 5)?;
1035
1036        self.console.borrow_mut().draw_tri_filled(x1y1, x2y2, x3y3)?;
1037        Ok(())
1038    }
1039}
1040
1041impl GfxSyncCommand {
1042    /// Creates a new `GFX_SYNC` command that controls video syncing on `console`.
1043    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
1044        Rc::from(Self {
1045            metadata: CallableMetadataBuilder::new("GFX_SYNC")
1046                .with_syntax(&[
1047                    (&[], None),
1048                    (
1049                        &[SingularArgSyntax::RequiredValue(
1050                            RequiredValueSyntax {
1051                                name: Cow::Borrowed("enabled"),
1052                                vtype: ExprType::Boolean,
1053                            },
1054                            ArgSepSyntax::End,
1055                        )],
1056                        None,
1057                    ),
1058                ])
1059                .with_category(CATEGORY)
1060                .with_description(
1061                    "Controls the video syncing flag and/or forces a sync.
1062With no arguments, this command triggers a video sync without updating the video syncing flag.  \
1063When enabled? is specified, this updates the video syncing flag accordingly and triggers a video \
1064sync if enabled? is TRUE.
1065When video syncing is enabled, all console commands immediately refresh the console.  This is \
1066useful to see the effects of the commands right away, which is why this is the default mode in the \
1067interpreter.  However, this is a *very* inefficient way of drawing.
1068When video syncing is disabled, all console updates are buffered until video syncing is enabled \
1069again.  This is perfect to draw complex graphics efficiently.  If this is what you want to do, \
1070you should disable syncing first, render a frame, call GFX_SYNC to flush the frame, repeat until \
1071you are done, and then enable video syncing again.  To keep a stable frame rate, use MONOTONIC \
1072to measure each frame and SLEEP only for the time left until the next one.  Note that the textual \
1073cursor is not visible when video syncing is disabled.
1074WARNING: Be aware that if you disable video syncing in the interactive interpreter, you will not \
1075be able to see what you are typing any longer until you reenable video syncing.",
1076                )
1077                .build(),
1078            console,
1079        })
1080    }
1081}
1082
1083impl Callable for GfxSyncCommand {
1084    fn metadata(&self) -> Rc<CallableMetadata> {
1085        self.metadata.clone()
1086    }
1087
1088    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
1089        if scope.nargs() == 0 {
1090            self.console.borrow_mut().sync_now()?;
1091            Ok(())
1092        } else {
1093            debug_assert_eq!(1, scope.nargs());
1094            let enabled = scope.get_boolean(0);
1095
1096            let mut console = self.console.borrow_mut();
1097            if enabled {
1098                console.show_cursor()?;
1099            } else {
1100                console.hide_cursor()?;
1101            }
1102            console.set_sync(enabled)?;
1103            Ok(())
1104        }
1105    }
1106}
1107
1108/// The `GFX_WIDTH` function.
1109pub struct GfxWidthFunction {
1110    metadata: Rc<CallableMetadata>,
1111    console: Rc<RefCell<dyn Console>>,
1112}
1113
1114impl GfxWidthFunction {
1115    /// Creates a new instance of the function.
1116    pub fn new(console: Rc<RefCell<dyn Console>>) -> Rc<Self> {
1117        Rc::from(Self {
1118            metadata: CallableMetadataBuilder::new("GFX_WIDTH")
1119                .with_return_type(ExprType::Integer)
1120                .with_syntax(&[(&[], None)])
1121                .with_category(CATEGORY)
1122                .with_description(
1123                    "Returns the width in pixels of the graphical console.
1124See GFX_HEIGHT to query the other dimension.",
1125                )
1126                .build(),
1127            console,
1128        })
1129    }
1130}
1131
1132impl Callable for GfxWidthFunction {
1133    fn metadata(&self) -> Rc<CallableMetadata> {
1134        self.metadata.clone()
1135    }
1136
1137    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
1138        debug_assert_eq!(0, scope.nargs());
1139        let size = self.console.borrow().size_pixels()?;
1140        scope.return_integer(i32::from(size.width))
1141    }
1142}
1143
1144/// Adds all console-related commands for the given `console` to the `machine`.
1145pub fn add_all(machine: &mut MachineBuilder, console: Rc<RefCell<dyn Console>>) {
1146    machine.add_callable(GfxCircleCommand::new(console.clone()));
1147    machine.add_callable(GfxCirclefCommand::new(console.clone()));
1148    machine.add_callable(GfxFillCommand::new(console.clone()));
1149    machine.add_callable(GfxHeightFunction::new(console.clone()));
1150    machine.add_callable(GfxLineCommand::new(console.clone()));
1151    machine.add_callable(GfxPeekFunction::new(console.clone()));
1152    machine.add_callable(GfxPixelCommand::new(console.clone()));
1153    machine.add_callable(GfxPolyCommand::new(console.clone()));
1154    machine.add_callable(GfxPolyfCommand::new(console.clone()));
1155    machine.add_callable(GfxRectCommand::new(console.clone()));
1156    machine.add_callable(GfxRectfCommand::new(console.clone()));
1157    machine.add_callable(GfxSyncCommand::new(console.clone()));
1158    machine.add_callable(GfxTriCommand::new(console.clone()));
1159    machine.add_callable(GfxTrifCommand::new(console.clone()));
1160    machine.add_callable(GfxWidthFunction::new(console));
1161}
1162
1163#[cfg(test)]
1164mod tests {
1165    use super::*;
1166    use crate::console::SizeInPixels;
1167    use crate::testutils::*;
1168
1169    /// Verifies error conditions for a command named `name` that takes two X/Y pairs.
1170    fn check_errors_two_xy(name: &'static str) {
1171        for args in &["1, 2, , 4", "1, 2, 3", "1, 2, 3, 4, 5", "2; 3, 4"] {
1172            check_stmt_compilation_err(
1173                format!("1:1: {} expected x1%, y1%, x2%, y2%", name),
1174                &format!("{} {}", name, args),
1175            );
1176        }
1177
1178        for args in &["-40000, 1, 1, 1", "1, -40000, 1, 1", "1, 1, -40000, 1", "1, 1, 1, -40000"] {
1179            let pos = name.len() + 1 + args.find('-').unwrap() + 1;
1180            check_stmt_err(
1181                format!("1:{}: Coordinate -40000 out of range", pos),
1182                &format!("{} {}", name, args),
1183            );
1184        }
1185
1186        for args in &["40000, 1, 1, 1", "1, 40000, 1, 1", "1, 1, 40000, 1", "1, 1, 1, 40000"] {
1187            let pos = name.len() + 1 + args.find('4').unwrap() + 1;
1188            check_stmt_err(
1189                format!("1:{}: Coordinate 40000 out of range", pos),
1190                &format!("{} {}", name, args),
1191            );
1192        }
1193
1194        for args in &["\"a\", 1, 1, 1", "1, \"a\", 1, 1", "1, 1, \"a\", 1", "1, 1, 1, \"a\""] {
1195            let stmt = &format!("{} {}", name, args);
1196            let pos = stmt.find('"').unwrap() + 1;
1197            check_stmt_compilation_err(format!("1:{}: STRING is not a number", pos), stmt);
1198        }
1199    }
1200
1201    /// Verifies error conditions for a command named `name` that takes three X/Y pairs.
1202    fn check_errors_three_xy(name: &'static str) {
1203        for args in &["1, 2, 3, 4, 5", "1, 2, 3, 4, 5, 6, 7", "1, 2, 3, 4, , 6"] {
1204            check_stmt_compilation_err(
1205                format!("1:1: {} expected x1%, y1%, x2%, y2%, x3%, y3%", name),
1206                &format!("{} {}", name, args),
1207            );
1208        }
1209
1210        check_stmt_compilation_err(
1211            format!("1:{}: {} expected x1%, y1%, x2%, y2%, x3%, y3%", name.len() + 3, name),
1212            &format!("{} 2; 3, 4, 5, 6, 7", name),
1213        );
1214
1215        for args in &[
1216            "-40000, 1, 1, 1, 1, 1",
1217            "1, -40000, 1, 1, 1, 1",
1218            "1, 1, -40000, 1, 1, 1",
1219            "1, 1, 1, -40000, 1, 1",
1220            "1, 1, 1, 1, -40000, 1",
1221            "1, 1, 1, 1, 1, -40000",
1222        ] {
1223            let pos = name.len() + 1 + args.find('-').unwrap() + 1;
1224            check_stmt_err(
1225                format!("1:{}: Coordinate -40000 out of range", pos),
1226                &format!("{} {}", name, args),
1227            );
1228        }
1229
1230        for args in &[
1231            "40000, 1, 1, 1, 1, 1",
1232            "1, 40000, 1, 1, 1, 1",
1233            "1, 1, 40000, 1, 1, 1",
1234            "1, 1, 1, 40000, 1, 1",
1235            "1, 1, 1, 1, 40000, 1",
1236            "1, 1, 1, 1, 1, 40000",
1237        ] {
1238            let pos = name.len() + 1 + args.find('4').unwrap() + 1;
1239            check_stmt_err(
1240                format!("1:{}: Coordinate 40000 out of range", pos),
1241                &format!("{} {}", name, args),
1242            );
1243        }
1244
1245        for args in &[
1246            "\"a\", 1, 1, 1, 1, 1",
1247            "1, \"a\", 1, 1, 1, 1",
1248            "1, 1, \"a\", 1, 1, 1",
1249            "1, 1, 1, \"a\", 1, 1",
1250            "1, 1, 1, 1, \"a\", 1",
1251            "1, 1, 1, 1, 1, \"a\"",
1252        ] {
1253            let stmt = &format!("{} {}", name, args);
1254            let pos = stmt.find('"').unwrap() + 1;
1255            check_stmt_compilation_err(format!("1:{}: STRING is not a number", pos), stmt);
1256        }
1257    }
1258
1259    /// Verifies error conditions for a command named `name` that takes zero or more X/Y pairs.
1260    fn check_errors_poly(name: &'static str) {
1261        let syntax = format!(
1262            "1:{}: {} expected <> | <points> | <x1%, y1%[, coord1%, .., coordN%]>",
1263            name.len() + 2,
1264            name,
1265        );
1266        check_stmt_compilation_err(syntax, &format!("{} 1", name));
1267        check_stmt_err(
1268            format!("1:{}: Expected an even number of coordinates", name.len() + 14),
1269            &format!("{} 1, 2, 3, 4, 5", name),
1270        );
1271
1272        check_stmt_compilation_err(
1273            format!(
1274                "1:{}: {} expected <> | <points> | <x1%, y1%[, coord1%, .., coordN%]>",
1275                name.len() + 3,
1276                name,
1277            ),
1278            &format!("{} 2; 3, 4", name),
1279        );
1280        check_stmt_compilation_err(
1281            format!(
1282                "1:{}: {} expected <> | <points> | <x1%, y1%[, coord1%, .., coordN%]>",
1283                name.len() + 8,
1284                name,
1285            ),
1286            &format!("{} 1, 2, , 4", name),
1287        );
1288
1289        for args in &["-40000, 1", "1, -40000", "1, 1, -40000, 2", "1, 1, 2, -40000"] {
1290            let pos = name.len() + 1 + args.find('-').unwrap() + 1;
1291            check_stmt_err(
1292                format!("1:{}: Coordinate -40000 out of range", pos),
1293                &format!("{} {}", name, args),
1294            );
1295        }
1296
1297        for args in &["40000, 1", "1, 40000", "1, 1, 40000, 2", "1, 1, 2, 40000"] {
1298            let pos = name.len() + 1 + args.find('4').unwrap() + 1;
1299            check_stmt_err(
1300                format!("1:{}: Coordinate 40000 out of range", pos),
1301                &format!("{} {}", name, args),
1302            );
1303        }
1304
1305        for args in &["\"a\", 1", "1, \"a\"", "1, 1, \"a\", 2", "1, 1, 2, \"a\""] {
1306            let stmt = &format!("{} {}", name, args);
1307            let pos = stmt.find('"').unwrap() + 1;
1308            check_stmt_compilation_err(format!("1:{}: STRING is not a number", pos), stmt);
1309        }
1310
1311        check_stmt_err(
1312            format!("1:{}: Expected an even number of coordinates", name.len() + 28),
1313            &format!("DIM points(5) AS INTEGER: {} points", name),
1314        );
1315        check_stmt_err(
1316            format!(
1317                "1:{}: Expected a flat array of coordinates or an Nx2 array of points",
1318                name.len() + 31
1319            ),
1320            &format!("DIM points(3, 3) AS INTEGER: {} points", name),
1321        );
1322        check_stmt_err(
1323            format!("1:{}: Expected an INTEGER array", name.len() + 28),
1324            &format!("DIM points(6) AS BOOLEAN: {} points", name),
1325        );
1326        check_stmt_err(
1327            format!("1:{}: Coordinate 40000 out of range", name.len() + 47),
1328            &format!("DIM points(2) AS INTEGER: points(0) = 40000: {} points", name),
1329        );
1330    }
1331
1332    /// Verifies error conditions for a command named `name` that takes an X/Y pair and a radius.
1333    fn check_errors_xy_radius(name: &'static str) {
1334        for args in &["1, , 3", "1, 2", "1, 2, 3, 4"] {
1335            check_stmt_compilation_err(
1336                format!("1:1: {} expected x%, y%, r%", name),
1337                &format!("{} {}", name, args),
1338            );
1339        }
1340        check_stmt_compilation_err(
1341            format!("1:{}: {} expected x%, y%, r%", name.len() + 3, name),
1342            &format!("{} 2; 3, 4", name),
1343        );
1344
1345        for args in &["-40000, 1, 1", "1, -40000, 1"] {
1346            let pos = name.len() + 1 + args.find('-').unwrap() + 1;
1347            check_stmt_err(
1348                format!("1:{}: Coordinate -40000 out of range", pos),
1349                &format!("{} {}", name, args),
1350            );
1351        }
1352        check_stmt_err(
1353            format!("1:{}: Radius -40000 must be positive", name.len() + 8),
1354            &format!("{} 1, 1, -40000", name),
1355        );
1356
1357        for args in &["40000, 1, 1", "1, 40000, 1"] {
1358            let pos = name.len() + 1 + args.find('4').unwrap() + 1;
1359            check_stmt_err(
1360                format!("1:{}: Coordinate 40000 out of range", pos),
1361                &format!("{} {}", name, args),
1362            );
1363        }
1364        check_stmt_err(
1365            format!("1:{}: Radius 80000 out of range", name.len() + 8),
1366            &format!("{} 1, 1, 80000", name),
1367        );
1368
1369        for args in &["\"a\", 1, 1", "1, \"a\", 1", "1, 1, \"a\""] {
1370            let stmt = &format!("{} {}", name, args);
1371            let pos = stmt.find('"').unwrap() + 1;
1372            check_stmt_compilation_err(format!("1:{}: STRING is not a number", pos), stmt);
1373        }
1374
1375        check_stmt_err(
1376            format!("1:{}: Radius -1 must be positive", name.len() + 8),
1377            &format!("{} 1, 1, -1", name),
1378        );
1379    }
1380
1381    #[test]
1382    fn test_gfx_circle_ok() {
1383        Tester::default()
1384            .run("GFX_CIRCLE 0, 0, 0")
1385            .expect_output([CapturedOut::DrawCircle(PixelsXY { x: 0, y: 0 }, 0)])
1386            .check();
1387
1388        Tester::default()
1389            .run("GFX_CIRCLE 1.1, 2.3, 2.5")
1390            .expect_output([CapturedOut::DrawCircle(PixelsXY { x: 1, y: 2 }, 3)])
1391            .check();
1392
1393        Tester::default()
1394            .run("GFX_CIRCLE -31000, -32000, 31000")
1395            .expect_output([CapturedOut::DrawCircle(PixelsXY { x: -31000, y: -32000 }, 31000)])
1396            .check();
1397    }
1398
1399    #[test]
1400    fn test_gfx_circle_errors() {
1401        check_errors_xy_radius("GFX_CIRCLE");
1402    }
1403
1404    #[test]
1405    fn test_gfx_circlef_ok() {
1406        Tester::default()
1407            .run("GFX_CIRCLEF 0, 0, 0")
1408            .expect_output([CapturedOut::DrawCircleFilled(PixelsXY { x: 0, y: 0 }, 0)])
1409            .check();
1410
1411        Tester::default()
1412            .run("GFX_CIRCLEF 1.1, 2.3, 2.5")
1413            .expect_output([CapturedOut::DrawCircleFilled(PixelsXY { x: 1, y: 2 }, 3)])
1414            .check();
1415
1416        Tester::default()
1417            .run("GFX_CIRCLEF -31000, -32000, 31000")
1418            .expect_output([CapturedOut::DrawCircleFilled(
1419                PixelsXY { x: -31000, y: -32000 },
1420                31000,
1421            )])
1422            .check();
1423    }
1424
1425    #[test]
1426    fn test_gfx_circlef_errors() {
1427        check_errors_xy_radius("GFX_CIRCLEF");
1428    }
1429
1430    #[test]
1431    fn test_gfx_fill_ok() {
1432        Tester::default()
1433            .run("GFX_FILL 1, 2")
1434            .expect_output([CapturedOut::BucketFill(PixelsXY { x: 1, y: 2 })])
1435            .check();
1436
1437        Tester::default()
1438            .run("GFX_FILL -31000, -32000")
1439            .expect_output([CapturedOut::BucketFill(PixelsXY { x: -31000, y: -32000 })])
1440            .check();
1441
1442        Tester::default()
1443            .run("GFX_FILL 30999.5, 31999.7")
1444            .expect_output([CapturedOut::BucketFill(PixelsXY { x: 31000, y: 32000 })])
1445            .check();
1446    }
1447
1448    #[test]
1449    fn test_gfx_fill_errors() {
1450        for cmd in &["GFX_FILL , 2", "GFX_FILL 1, 2, 3", "GFX_FILL 1"] {
1451            check_stmt_compilation_err("1:1: GFX_FILL expected x%, y%", cmd);
1452        }
1453        check_stmt_compilation_err("1:11: GFX_FILL expected x%, y%", "GFX_FILL 1; 2");
1454
1455        for cmd in &["GFX_FILL -40000, 1", "GFX_FILL 1, -40000"] {
1456            check_stmt_err(
1457                format!("1:{}: Coordinate -40000 out of range", cmd.find('-').unwrap() + 1),
1458                cmd,
1459            );
1460        }
1461
1462        for cmd in &["GFX_FILL \"a\", 1", "GFX_FILL 1, \"a\""] {
1463            let pos = cmd.find('"').unwrap() + 1;
1464            check_stmt_compilation_err(format!("1:{}: STRING is not a number", pos), cmd);
1465        }
1466    }
1467
1468    #[test]
1469    fn test_gfx_height() {
1470        let mut t = Tester::default();
1471        t.get_console().borrow_mut().set_size_pixels(SizeInPixels::new(1, 768));
1472        t.run("result = GFX_HEIGHT").expect_var("result", 768i32).check();
1473
1474        check_expr_error("1:10: Graphical console size not yet set", "GFX_HEIGHT");
1475
1476        check_expr_compilation_error("1:10: GFX_HEIGHT expected no arguments", "GFX_HEIGHT()");
1477        check_expr_compilation_error("1:10: GFX_HEIGHT expected no arguments", "GFX_HEIGHT(1)");
1478    }
1479
1480    #[test]
1481    fn test_gfx_line_ok() {
1482        Tester::default()
1483            .run("GFX_LINE 1, 2, 3, 4")
1484            .expect_output([CapturedOut::DrawLine(
1485                PixelsXY { x: 1, y: 2 },
1486                PixelsXY { x: 3, y: 4 },
1487            )])
1488            .check();
1489
1490        Tester::default()
1491            .run("GFX_LINE -31000.3, -32000.2, 31000.4, 31999.8")
1492            .expect_output([CapturedOut::DrawLine(
1493                PixelsXY { x: -31000, y: -32000 },
1494                PixelsXY { x: 31000, y: 32000 },
1495            )])
1496            .check();
1497    }
1498
1499    #[test]
1500    fn test_gfx_line_errors() {
1501        check_errors_two_xy("GFX_LINE");
1502    }
1503
1504    #[test]
1505    fn test_gfx_pixel_ok() {
1506        Tester::default()
1507            .run("GFX_PIXEL 1, 2")
1508            .expect_output([CapturedOut::DrawPixel(PixelsXY { x: 1, y: 2 })])
1509            .check();
1510
1511        Tester::default()
1512            .run("GFX_PIXEL -31000, -32000")
1513            .expect_output([CapturedOut::DrawPixel(PixelsXY { x: -31000, y: -32000 })])
1514            .check();
1515
1516        Tester::default()
1517            .run("GFX_PIXEL 30999.5, 31999.7")
1518            .expect_output([CapturedOut::DrawPixel(PixelsXY { x: 31000, y: 32000 })])
1519            .check();
1520    }
1521
1522    #[test]
1523    fn test_gfx_pixel_errors() {
1524        for cmd in &["GFX_PIXEL , 2", "GFX_PIXEL 1, 2, 3", "GFX_PIXEL 1"] {
1525            check_stmt_compilation_err("1:1: GFX_PIXEL expected x%, y%", cmd);
1526        }
1527        check_stmt_compilation_err("1:12: GFX_PIXEL expected x%, y%", "GFX_PIXEL 1; 2");
1528
1529        for cmd in &["GFX_PIXEL -40000, 1", "GFX_PIXEL 1, -40000"] {
1530            check_stmt_err(
1531                format!("1:{}: Coordinate -40000 out of range", cmd.find('-').unwrap() + 1),
1532                cmd,
1533            );
1534        }
1535
1536        for cmd in &["GFX_PIXEL \"a\", 1", "GFX_PIXEL 1, \"a\""] {
1537            let pos = cmd.find('"').unwrap() + 1;
1538            check_stmt_compilation_err(format!("1:{}: STRING is not a number", pos), cmd);
1539        }
1540    }
1541
1542    #[test]
1543    fn test_gfx_peek_ok() {
1544        let mut t = Tester::default();
1545        t.get_console().borrow_mut().set_peek_pixel(PixelsXY::new(1, 2), Some(7));
1546        t.run("result = GFX_PEEK(1, 2)").expect_var("result", 7i32).check();
1547
1548        let mut t = Tester::default();
1549        t.get_console().borrow_mut().set_peek_pixel(PixelsXY::new(1, 2), None);
1550        t.run("result = GFX_PEEK(1, 2)").expect_var("result", -1i32).check();
1551
1552        Tester::default()
1553            .run("result = GFX_PEEK(-31000.2, 31999.7)")
1554            .expect_var("result", -1i32)
1555            .check();
1556    }
1557
1558    #[test]
1559    fn test_gfx_peek_errors() {
1560        for expr in &["GFX_PEEK()", "GFX_PEEK(1)", "GFX_PEEK(1, 2, 3)"] {
1561            check_expr_compilation_error("1:10: GFX_PEEK expected x%, y%", expr);
1562        }
1563        check_expr_compilation_error("1:20: Unexpected ;", "GFX_PEEK(1; 2)");
1564
1565        for expr in &["GFX_PEEK(-40000, 1)", "GFX_PEEK(1, -40000)"] {
1566            let pos = expr.find('-').unwrap() + 10;
1567            check_expr_error(format!("1:{}: Coordinate -40000 out of range", pos), expr);
1568        }
1569
1570        for expr in &["GFX_PEEK(40000, 1)", "GFX_PEEK(1, 40000)"] {
1571            let pos = expr.find('4').unwrap() + 10;
1572            check_expr_error(format!("1:{}: Coordinate 40000 out of range", pos), expr);
1573        }
1574
1575        for expr in &[r#"GFX_PEEK("a", 1)"#, r#"GFX_PEEK(1, "a")"#] {
1576            let pos = expr.find('"').unwrap() + 10;
1577            check_expr_compilation_error(format!("1:{}: STRING is not a number", pos), expr);
1578        }
1579    }
1580
1581    #[test]
1582    fn test_gfx_poly_ok() {
1583        Tester::default().run("GFX_POLY").expect_output([]).check();
1584
1585        Tester::default()
1586            .run("GFX_POLY 1.1, 2.3")
1587            .expect_output([CapturedOut::DrawPixel(PixelsXY { x: 1, y: 2 })])
1588            .check();
1589
1590        Tester::default()
1591            .run("GFX_POLY 1.1, 2.3, 2.5, 3.9")
1592            .expect_output([CapturedOut::DrawLine(
1593                PixelsXY { x: 1, y: 2 },
1594                PixelsXY { x: 3, y: 4 },
1595            )])
1596            .check();
1597
1598        Tester::default()
1599            .run("GFX_POLY 1.1, 2.3, 2.5, 3.9, 4.4, 5.6")
1600            .expect_output([CapturedOut::DrawTri(
1601                PixelsXY { x: 1, y: 2 },
1602                PixelsXY { x: 3, y: 4 },
1603                PixelsXY { x: 4, y: 6 },
1604            )])
1605            .check();
1606
1607        Tester::default()
1608            .run("GFX_POLY -31000, -32000, 31000, -32000, 0, 32000, -1, 0")
1609            .expect_output([CapturedOut::DrawPoly(vec![
1610                PixelsXY { x: -31000, y: -32000 },
1611                PixelsXY { x: 31000, y: -32000 },
1612                PixelsXY { x: 0, y: 32000 },
1613                PixelsXY { x: -1, y: 0 },
1614            ])])
1615            .check();
1616
1617        Tester::default()
1618            .run(
1619                "DIM points(6) AS INTEGER\
1620                : points(0) = 1\
1621                : points(1) = 2\
1622                : points(2) = 3\
1623                : points(3) = 4\
1624                : points(4) = 5\
1625                : points(5) = 6\
1626                : GFX_POLY points",
1627            )
1628            .expect_output([CapturedOut::DrawTri(
1629                PixelsXY { x: 1, y: 2 },
1630                PixelsXY { x: 3, y: 4 },
1631                PixelsXY { x: 5, y: 6 },
1632            )])
1633            .check();
1634
1635        Tester::default()
1636            .run(
1637                "DIM points(3, 2) AS INTEGER\
1638                : points(0, 0) = 1\
1639                : points(0, 1) = 2\
1640                : points(1, 0) = 3\
1641                : points(1, 1) = 4\
1642                : points(2, 0) = 5\
1643                : points(2, 1) = 6\
1644                : GFX_POLY points",
1645            )
1646            .expect_output([CapturedOut::DrawTri(
1647                PixelsXY { x: 1, y: 2 },
1648                PixelsXY { x: 3, y: 4 },
1649                PixelsXY { x: 5, y: 6 },
1650            )])
1651            .check();
1652    }
1653
1654    #[test]
1655    fn test_gfx_poly_errors() {
1656        check_errors_poly("GFX_POLY");
1657    }
1658
1659    #[test]
1660    fn test_gfx_polyf_ok() {
1661        Tester::default().run("GFX_POLYF").expect_output([]).check();
1662
1663        Tester::default()
1664            .run("GFX_POLYF 1.1, 2.3")
1665            .expect_output([CapturedOut::DrawPixel(PixelsXY { x: 1, y: 2 })])
1666            .check();
1667
1668        Tester::default()
1669            .run("GFX_POLYF 1.1, 2.3, 2.5, 3.9")
1670            .expect_output([CapturedOut::DrawLine(
1671                PixelsXY { x: 1, y: 2 },
1672                PixelsXY { x: 3, y: 4 },
1673            )])
1674            .check();
1675
1676        Tester::default()
1677            .run("GFX_POLYF 1.1, 2.3, 2.5, 3.9, 4.4, 5.6")
1678            .expect_output([CapturedOut::DrawTriFilled(
1679                PixelsXY { x: 1, y: 2 },
1680                PixelsXY { x: 3, y: 4 },
1681                PixelsXY { x: 4, y: 6 },
1682            )])
1683            .check();
1684
1685        Tester::default()
1686            .run("GFX_POLYF -31000, -32000, 31000, -32000, 0, 32000, -1, 0")
1687            .expect_output([CapturedOut::DrawPolyFilled(vec![
1688                PixelsXY { x: -31000, y: -32000 },
1689                PixelsXY { x: 31000, y: -32000 },
1690                PixelsXY { x: 0, y: 32000 },
1691                PixelsXY { x: -1, y: 0 },
1692            ])])
1693            .check();
1694
1695        Tester::default()
1696            .run(
1697                "DIM points(6) AS INTEGER\
1698                : points(0) = 1\
1699                : points(1) = 2\
1700                : points(2) = 3\
1701                : points(3) = 4\
1702                : points(4) = 5\
1703                : points(5) = 6\
1704                : GFX_POLYF points",
1705            )
1706            .expect_output([CapturedOut::DrawTriFilled(
1707                PixelsXY { x: 1, y: 2 },
1708                PixelsXY { x: 3, y: 4 },
1709                PixelsXY { x: 5, y: 6 },
1710            )])
1711            .check();
1712
1713        Tester::default()
1714            .run(
1715                "DIM points(3, 2) AS INTEGER\
1716                : points(0, 0) = 1\
1717                : points(0, 1) = 2\
1718                : points(1, 0) = 3\
1719                : points(1, 1) = 4\
1720                : points(2, 0) = 5\
1721                : points(2, 1) = 6\
1722                : GFX_POLYF points",
1723            )
1724            .expect_output([CapturedOut::DrawTriFilled(
1725                PixelsXY { x: 1, y: 2 },
1726                PixelsXY { x: 3, y: 4 },
1727                PixelsXY { x: 5, y: 6 },
1728            )])
1729            .check();
1730    }
1731
1732    #[test]
1733    fn test_gfx_polyf_errors() {
1734        check_errors_poly("GFX_POLYF");
1735    }
1736
1737    #[test]
1738    fn test_gfx_rect_ok() {
1739        Tester::default()
1740            .run("GFX_RECT 1.1, 2.3, 2.5, 3.9")
1741            .expect_output([CapturedOut::DrawRect(
1742                PixelsXY { x: 1, y: 2 },
1743                PixelsXY { x: 3, y: 4 },
1744            )])
1745            .check();
1746
1747        Tester::default()
1748            .run("GFX_RECT -31000, -32000, 31000, 32000")
1749            .expect_output([CapturedOut::DrawRect(
1750                PixelsXY { x: -31000, y: -32000 },
1751                PixelsXY { x: 31000, y: 32000 },
1752            )])
1753            .check();
1754    }
1755
1756    #[test]
1757    fn test_gfx_rect_errors() {
1758        check_errors_two_xy("GFX_RECT");
1759    }
1760
1761    #[test]
1762    fn test_gfx_rectf_ok() {
1763        Tester::default()
1764            .run("GFX_RECTF 1.1, 2.3, 2.5, 3.9")
1765            .expect_output([CapturedOut::DrawRectFilled(
1766                PixelsXY { x: 1, y: 2 },
1767                PixelsXY { x: 3, y: 4 },
1768            )])
1769            .check();
1770
1771        Tester::default()
1772            .run("GFX_RECTF -31000, -32000, 31000, 32000")
1773            .expect_output([CapturedOut::DrawRectFilled(
1774                PixelsXY { x: -31000, y: -32000 },
1775                PixelsXY { x: 31000, y: 32000 },
1776            )])
1777            .check();
1778    }
1779
1780    #[test]
1781    fn test_gfx_rectf_errors() {
1782        check_errors_two_xy("GFX_RECTF");
1783    }
1784
1785    #[test]
1786    fn test_gfx_sync_ok() {
1787        Tester::default().run("GFX_SYNC").expect_output([CapturedOut::SyncNow]).check();
1788        Tester::default()
1789            .run("GFX_SYNC TRUE")
1790            .expect_output([CapturedOut::ShowCursor, CapturedOut::SetSync(true)])
1791            .check();
1792        Tester::default()
1793            .run("GFX_SYNC FALSE")
1794            .expect_output([CapturedOut::HideCursor, CapturedOut::SetSync(false)])
1795            .check();
1796    }
1797
1798    #[test]
1799    fn test_gfx_sync_errors() {
1800        check_stmt_compilation_err("1:1: GFX_SYNC expected <> | <enabled?>", "GFX_SYNC 2, 3");
1801        check_stmt_compilation_err("1:10: Expected BOOLEAN but found INTEGER", "GFX_SYNC 2");
1802    }
1803
1804    #[test]
1805    fn test_gfx_tri_ok() {
1806        Tester::default()
1807            .run("GFX_TRI 1.1, 2.3, 2.5, 3.9, 4.4, 5.6")
1808            .expect_output([CapturedOut::DrawTri(
1809                PixelsXY { x: 1, y: 2 },
1810                PixelsXY { x: 3, y: 4 },
1811                PixelsXY { x: 4, y: 6 },
1812            )])
1813            .check();
1814
1815        Tester::default()
1816            .run("GFX_TRI -31000, -32000, 31000, -32000, 0, 32000")
1817            .expect_output([CapturedOut::DrawTri(
1818                PixelsXY { x: -31000, y: -32000 },
1819                PixelsXY { x: 31000, y: -32000 },
1820                PixelsXY { x: 0, y: 32000 },
1821            )])
1822            .check();
1823    }
1824
1825    #[test]
1826    fn test_gfx_tri_errors() {
1827        check_errors_three_xy("GFX_TRI");
1828    }
1829
1830    #[test]
1831    fn test_gfx_trif_ok() {
1832        Tester::default()
1833            .run("GFX_TRIF 1.1, 2.3, 2.5, 3.9, 4.4, 5.6")
1834            .expect_output([CapturedOut::DrawTriFilled(
1835                PixelsXY { x: 1, y: 2 },
1836                PixelsXY { x: 3, y: 4 },
1837                PixelsXY { x: 4, y: 6 },
1838            )])
1839            .check();
1840
1841        Tester::default()
1842            .run("GFX_TRIF -31000, -32000, 31000, -32000, 0, 32000")
1843            .expect_output([CapturedOut::DrawTriFilled(
1844                PixelsXY { x: -31000, y: -32000 },
1845                PixelsXY { x: 31000, y: -32000 },
1846                PixelsXY { x: 0, y: 32000 },
1847            )])
1848            .check();
1849    }
1850
1851    #[test]
1852    fn test_gfx_trif_errors() {
1853        check_errors_three_xy("GFX_TRIF");
1854    }
1855
1856    #[test]
1857    fn test_gfx_width() {
1858        let mut t = Tester::default();
1859        t.get_console().borrow_mut().set_size_pixels(SizeInPixels::new(12345, 1));
1860        t.run("result = GFX_WIDTH").expect_var("result", 12345i32).check();
1861
1862        check_expr_error("1:10: Graphical console size not yet set", "GFX_WIDTH");
1863
1864        check_expr_compilation_error("1:10: GFX_WIDTH expected no arguments", "GFX_WIDTH()");
1865        check_expr_compilation_error("1:10: GFX_WIDTH expected no arguments", "GFX_WIDTH(1)");
1866    }
1867}