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