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