From b8614162cdbb3817a7a58ceadf8b6b0d05fae952 Mon Sep 17 00:00:00 2001
From: Frederico Linhares <fred@linhares.blue>
Date: Wed, 24 May 2023 14:50:01 -0300
Subject: feat Recreate the Menu

---
 lib/menu.rb            | 220 +++++++++++++++++++++++++++++++++++++++++++++++++
 test/Rakefile          |   9 +-
 test/src/main.rb       | 107 ++++++------------------
 test/src/mode/demo.rb  | 113 +++++++++++++++++++++++++
 test/src/mode/title.rb |  49 +++++++++++
 test/textures/menu.qoi | Bin 0 -> 849 bytes
 6 files changed, 416 insertions(+), 82 deletions(-)
 create mode 100644 lib/menu.rb
 create mode 100644 test/src/mode/demo.rb
 create mode 100644 test/src/mode/title.rb
 create mode 100644 test/textures/menu.qoi

diff --git a/lib/menu.rb b/lib/menu.rb
new file mode 100644
index 0000000..9f4c55c
--- /dev/null
+++ b/lib/menu.rb
@@ -0,0 +1,220 @@
+# Copyright 2022-2023 Frederico de Oliveira Linhares
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+module CandyGear
+  class Menu
+    class Stack
+      def initialize(menu)
+        @stack = [];
+        @stack << menu;
+      end
+
+      def push(menu) = @stack << menu;
+
+      def pop()
+        @stack.pop() if @stack.size > 1;
+      end
+
+      def draw() = @stack.each {_1.draw();}
+      def size() = @stack.size;
+    end
+
+    class BorderlessView
+      attr_reader(:texture, :font, :text_color, :bg_color, :sprites);
+
+      def initialize(texture, font, sprite_width, sprite_height)
+        @texture = texture;
+        @font = font;
+        @sprite_width = sprite_width;
+        @sprite_height = sprite_height;
+
+        @sprites = {
+          arrow_select: Sprite.new(
+            @texture, Vector4D.new(1.0/3.0, 1.0/3.0, 2.0/3.0, 2.0/3.0))
+        }
+      end
+
+      def draw(x, y, width, height)
+        # Nothing.
+      end
+
+      def border_width() = 0;
+      def border_height() = 0;
+    end
+
+    class BorderedView
+      attr_reader(:texture, :font, :sprites, :border_width, :border_height);
+
+      def initialize(texture, font, sprite_width, sprite_height)
+        @texture = texture;
+        @font = font;
+        @border_width = sprite_width;
+        @border_height = sprite_height;
+
+        @sprites = {
+          arrow_select: Sprite.new(
+            @texture, Vector4D.new(1.0/3.0, 1.0/3.0, 2.0/3.0, 2.0/3.0)),
+          box_top_left: Sprite.new(
+            @texture, Vector4D.new(0.0, 0.0, 1.0/3.0, 1.0/3.0)),
+          box_top: Sprite.new(
+            @texture, Vector4D.new(1.1/3.0, 0.0, 1.9/3.0, 1.0/3.0)),
+          box_top_right: Sprite.new(
+            @texture, Vector4D.new(2.0/3.0, 0.0, 1.0, 1.0/3.0)),
+          box_left: Sprite.new(
+            @texture, Vector4D.new(0.0, 1.1/3.0, 1.0/3.0, 1.9/3.0)),
+          box_right: Sprite.new(
+            @texture, Vector4D.new(2.0/3.0, 1.1/3.0, 1.0, 1.9/3.0)),
+          box_bottom_left: Sprite.new(
+            @texture, Vector4D.new(0.0, 2.0/3.0, 1.0/3.0, 1.0)),
+          box_bottom: Sprite.new(
+            @texture, Vector4D.new(1.1/3.0, 2.0/3.0, 1.9/3.0, 1.0)),
+          box_bottom_right: Sprite.new(
+            @texture, Vector4D.new(2.0/3.0, 2.0/3.0, 1.0, 1.0))
+        }
+      end
+
+      def draw(view, x, y, width, height)
+        num_horizontal_sprites = width / @border_width + 1;
+        num_horizontal_sprites += 1 if width % @border_width > 0;
+        num_vertical_sprites = height / @border_height;
+        num_vertical_sprites += 1 if height % @border_height > 0;
+
+        # Draw the corners.
+        @sprites[:box_top_left].draw(
+          view, Vector4D.new(x, y, border_width, border_height));
+        @sprites[:box_top_right].draw(
+          view, Vector4D.new(
+            @border_width * (num_horizontal_sprites + 1) + x, y,
+            border_width, border_height));
+        @sprites[:box_bottom_left].draw(
+          view, Vector4D.new(
+            x, @border_height * (num_vertical_sprites + 1) + y,
+            border_width, border_height));
+        @sprites[:box_bottom_right].draw(
+          view, Vector4D.new(
+            @border_width * (num_horizontal_sprites + 1) + x,
+            @border_height * (num_vertical_sprites + 1) + y,
+            border_width, border_height));
+
+        # Draw the edges.
+        num_horizontal_sprites.times do |i|
+          # Top
+          @sprites[:box_top].draw(
+            view, Vector4D.new(@border_width * (i + 1) + x, y,
+                               border_width, border_height));
+          # Bottom
+          @sprites[:box_bottom].draw(
+            view, Vector4D.new(@border_width * (i + 1) + x,
+                               @border_height * (num_vertical_sprites + 1) + y,
+                               border_width, border_height));
+        end
+        num_vertical_sprites.times do |i|
+          # Left
+          @sprites[:box_left].draw(
+            view, Vector4D.new(
+              x, @border_height * (i + 1) + y, border_width, border_height));
+          # Right
+          @sprites[:box_right].draw(
+            view, Vector4D.new(
+              @border_width * (num_horizontal_sprites + 1) + x,
+              @border_height * (i + 1) + y,
+              border_width, border_height));
+        end
+      end
+    end
+
+    class Option
+      attr_reader(:action, :text, :width, :height);
+
+      def initialize(text, action, width, height);
+        @text = text;
+        @action = action;
+        @width = width;
+        @height = height;
+      end
+    end
+
+    attr_reader(:width, :height);
+
+    def initialize(view, menu_view, pos_x, pos_y, options)
+      @view = view;
+      @menu_view = menu_view;
+
+      @pos_x = pos_x;
+      @pos_y = pos_y;
+
+      @options = options.map do |opt|
+        texture = Texture.from_text(menu_view.font, opt[:text])
+        Option.new(
+          Sprite.new(texture, CandyGear::Vector4D.new(0, 0, 1.0, 1.0)),
+          opt[:action],
+          texture.width,
+          texture.height);
+      end
+      @current_option = 0;
+      @option_max_width = 0;
+      @option_max_height = 0;
+
+      @options.each do |i|
+        if @option_max_width < i.width then
+          @option_max_width = i.width;
+        end
+        if @option_max_height < i.height then
+          @option_max_height = i.height;
+        end
+      end
+
+      @width = @option_max_width;
+      @height = @option_max_height * @options.size;
+    end
+
+    def next_opt()
+      @current_option += 1;
+      @current_option = 0 if @current_option >= @options.size();
+    end
+
+    def pred_opt()
+      if @current_option <= 0 then
+        @current_option = @options.size - 1;
+      else
+        @current_option -= 1;
+      end
+    end
+
+    def activate()
+      @options[@current_option].action.call();
+    end
+
+    def draw()
+      @menu_view.draw(@view, @pos_x, @pos_y, @width, @height);
+
+      @options.each_with_index do |opt, i|
+        opt.text.draw(
+          @view, Vector4D.new(
+            @pos_x + @menu_view.border_width +
+            @menu_view.border_width,
+            @pos_y + @menu_view.border_height +
+            @option_max_height * i,
+            opt.width, opt.height));
+      end
+
+      @menu_view.sprites[:arrow_select].draw(
+        @view, Vector4D.new(
+          @pos_x + @menu_view.border_width,
+          @pos_y + @menu_view.border_height +
+          @option_max_height * @current_option,
+          @menu_view.border_width, @menu_view.border_height));
+    end
+  end
+end
diff --git a/test/Rakefile b/test/Rakefile
index c449fad..c219bca 100644
--- a/test/Rakefile
+++ b/test/Rakefile
@@ -13,11 +13,18 @@
 # limitations under the License.
 
 OBJ = 'test'
+SOURCE_DIR = ENV['DESTDIR'] || ''
+
+RB_LIBS_PATH = "#{SOURCE_DIR}/usr/local/share/candy_gear/lib"
+RB_LIBS = [
+  'menu'
+]
 
 RB_FILES = FileList['src/**/*.rb']
 
 task :build do
-  rb_files = RB_FILES.inject('') {_1 + "#{_2} "}
+  rb_files = RB_FILES.inject('') {_1 + "#{_2} "} +
+             RB_LIBS.inject('') {_1 + "#{RB_LIBS_PATH}/#{_2}.rb "}
 
   `mrbc -g -o #{OBJ}.mrb #{rb_files}`
 end
diff --git a/test/src/main.rb b/test/src/main.rb
index bc28ffc..98dcb9f 100644
--- a/test/src/main.rb
+++ b/test/src/main.rb
@@ -12,104 +12,49 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-CAMERA_ROTATION_SPEED = Math::PI/45;
-BOX_ROTATION_SPEED = Math::PI/180;
-TRANSLATION_SPEED = 0.5;
+def change_mode(new_mode)
+  $next_stage = new_mode;
+  $quit_stage = true;
+end
 
 def config()
   CandyGear.game_name = "Candy Gear Test";
   CandyGear::Graphic.display_width = 1280;
   CandyGear::Graphic.display_height = 720;
+  CandyGear::Graphic.fps = 60;
 end
 
 def init()
-  texture = CandyGear::Texture.from_image("textures/color_texture.qoi");
-  mesh = CandyGear::Mesh.new("meshes/cube.cgmesh");
+  menu_texture = CandyGear::Texture.from_image("textures/menu.qoi");
   # FIXME: Text rendering crashes with this font:
-  # font = CandyGear::Font.new("/usr/share/fonts/TTF/sazanami-mincho.ttf", 16);
+  # font = CandyGear::Font.new(
+  #   "/usr/share/fonts/TTF/sazanami-mincho.ttf", 16);
   # FIXME: This font, under this path, may not be present in all Linuxes:
-  font = CandyGear::Font.new("/usr/share/fonts/TTF/HanaMinA.ttf", 30);
-  japanese_text = CandyGear::Texture.from_text(font, "こんにちは世界!");
-  english_text = CandyGear::Texture.from_text(
-    font, "The quick brown fox jumps");
-
-  $color = CandyGear::Vector3D.new(0.8, 0.2, 0.2);
-  $model = CandyGear::Model.new(mesh, texture);
-  $sprite = CandyGear::Sprite.new(
-    texture, CandyGear::Vector4D.new(0, 0, 1.0, 1.0));
-  $rectangle = CandyGear::Vector4D.new(103.0, 1.0, 100.0, 100.0);
-  $sprite_position = CandyGear::Vector4D.new(1.0, 1.0, 100.0, 100.0);
-  $japanese_text_sprite = CandyGear::Sprite.new(
-    japanese_text, CandyGear::Vector4D.new(0, 0, 1.0, 1.0));
-  $japanese_text_position = CandyGear::Vector4D.new(
-    204.0, 1.0, japanese_text.width, japanese_text.height);
-  $english_text_sprite = CandyGear::Sprite.new(
-    english_text, CandyGear::Vector4D.new(0, 0, 1.0, 1.0));
-  $english_text_position = CandyGear::Vector4D.new(
-    204.0, japanese_text.height + 2.0,
-    english_text.width, english_text.height);
-
-  $instances = [
-    CandyGear::Vector3D.new(5.0, 0.0, 0.0),
-    CandyGear::Vector3D.new(-5.0, 0.0, 0.0),
-    CandyGear::Vector3D.new(0.0, 5.0, 0.0),
-    CandyGear::Vector3D.new(0.0, -5.0, 0.0),
-    CandyGear::Vector3D.new(0.0, 0.0, 5.0),
-    CandyGear::Vector3D.new(0.0, 0.0, -5.0)
-  ];
-  $instances_rotation = CandyGear::Rotation3D.new(0.0, 0.0, 0.0);
-
-  $camera_position = CandyGear::Vector3D.new(0.0, 0.0, 0.0);
-  $camera_rotation = CandyGear::Rotation3D.new(0.0, 0.0, 0.0);
-
-  color = CandyGear::Vector3D.new(0.12, 0.12, 0.18);
-  $view1 = CandyGear::View2D.new(
-    CandyGear::Vector4D.new(0, 0, 1280, 240), 640, 120);
-  $view2 = CandyGear::View3D.new(
-    CandyGear::Vector4D.new(0, 240, 1280, 480), 1280, 480);
-  CandyGear.views = [$view1, $view2];
+  font = CandyGear::Font.new("/usr/share/fonts/TTF/HanaMinA.ttf", 18);
+  $global_data = {
+    font: font,
+    menu_view: CandyGear::Menu::BorderedView.new(menu_texture, font, 16, 16)
+  }
 
-  $view2.camera_position = $camera_position;
-  $view2.camera_rotation = $camera_rotation;
+  change_mode(:title);
 end
 
-def key_down(key)
-  case key
-  when CandyGear::Key::I
-    $camera_rotation.rotate(-CAMERA_ROTATION_SPEED, 0.0);
-  when CandyGear::Key::K
-    $camera_rotation.rotate(CAMERA_ROTATION_SPEED, 0.0);
-  when CandyGear::Key::J
-    $camera_rotation.rotate(0.0, CAMERA_ROTATION_SPEED);
-  when CandyGear::Key::L
-    $camera_rotation.rotate(0.0, -CAMERA_ROTATION_SPEED);
-  when CandyGear::Key::E
-    $camera_position.translate(
-      CandyGear::Vector3D.new(0.0, 0.0, -TRANSLATION_SPEED), $camera_rotation);
-  when CandyGear::Key::D
-    $camera_position.translate(
-      CandyGear::Vector3D.new(0.0, 0.0, TRANSLATION_SPEED), $camera_rotation);
-  when CandyGear::Key::S
-    $camera_position.translate(
-      CandyGear::Vector3D.new(-TRANSLATION_SPEED, 0.0, 0.0), $camera_rotation);
-  when CandyGear::Key::F
-    $camera_position.translate(
-      CandyGear::Vector3D.new(TRANSLATION_SPEED, 0.0, 0.0), $camera_rotation);
-  end
-end
+def key_down(key) = $mode.key_down(key);
 
-def key_up(key)
-end
+def key_up(key) = $mode.key_up(key);
 
 def quit() = CandyGear.quit();
 
 def tick()
-  $sprite.draw($view1, $sprite_position);
-  $japanese_text_sprite.draw($view1, $japanese_text_position);
-  $english_text_sprite.draw($view1, $english_text_position);
-  $instances_rotation.rotate(0.0, BOX_ROTATION_SPEED);
-  $rectangle.draw_rectangle($view1, $color);
-  $instances.each do |i|
-    $model.draw(i, $instances_rotation);
+  if $quit_stage then
+    case $next_stage
+    when :title
+      $mode = Mode::Title.new();
+    else
+      $mode = Mode::Demo.new();
+    end
+    $quit_stage = false;
   end
+
+  $mode.tick();
 end
diff --git a/test/src/mode/demo.rb b/test/src/mode/demo.rb
new file mode 100644
index 0000000..b3be78d
--- /dev/null
+++ b/test/src/mode/demo.rb
@@ -0,0 +1,113 @@
+# Copyright 2022-2023 Frederico de Oliveira Linhares
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+module Mode
+  class Demo
+    CAMERA_ROTATION_SPEED = Math::PI/45;
+    BOX_ROTATION_SPEED = Math::PI/360;
+    TRANSLATION_SPEED = 0.5;
+
+    def initialize()
+      texture = CandyGear::Texture.from_image("textures/color_texture.qoi");
+      mesh = CandyGear::Mesh.new("meshes/cube.cgmesh");
+      font = CandyGear::Font.new("/usr/share/fonts/TTF/HanaMinA.ttf", 30);
+      japanese_text = CandyGear::Texture.from_text(
+        font, "こんにちは世界!");
+      english_text = CandyGear::Texture.from_text(
+        font, "The quick brown fox jumps");
+
+      @color = CandyGear::Vector3D.new(0.8, 0.2, 0.2);
+      @model = CandyGear::Model.new(mesh, texture);
+      @sprite = CandyGear::Sprite.new(
+        texture, CandyGear::Vector4D.new(0, 0, 1.0, 1.0));
+      @rectangle = CandyGear::Vector4D.new(103.0, 1.0, 100.0, 100.0);
+      @sprite_position = CandyGear::Vector4D.new(1.0, 1.0, 100.0, 100.0);
+      @japanese_text_sprite = CandyGear::Sprite.new(
+        japanese_text, CandyGear::Vector4D.new(0, 0, 1.0, 1.0));
+      @japanese_text_position = CandyGear::Vector4D.new(
+        204.0, 1.0, japanese_text.width, japanese_text.height);
+      @english_text_sprite = CandyGear::Sprite.new(
+        english_text, CandyGear::Vector4D.new(0, 0, 1.0, 1.0));
+      @english_text_position = CandyGear::Vector4D.new(
+        204.0, japanese_text.height + 2.0,
+        english_text.width, english_text.height);
+
+      @instances = [
+        CandyGear::Vector3D.new(5.0, 0.0, 0.0),
+        CandyGear::Vector3D.new(-5.0, 0.0, 0.0),
+        CandyGear::Vector3D.new(0.0, 5.0, 0.0),
+        CandyGear::Vector3D.new(0.0, -5.0, 0.0),
+        CandyGear::Vector3D.new(0.0, 0.0, 5.0),
+        CandyGear::Vector3D.new(0.0, 0.0, -5.0)
+      ];
+      @instances_rotation = CandyGear::Rotation3D.new(0.0, 0.0, 0.0);
+
+      @camera_position = CandyGear::Vector3D.new(0.0, 0.0, 0.0);
+      @camera_rotation = CandyGear::Rotation3D.new(0.0, 0.0, 0.0);
+
+      color = CandyGear::Vector3D.new(0.12, 0.12, 0.18);
+      @view1 = CandyGear::View2D.new(
+        CandyGear::Vector4D.new(0, 0, 1280, 240), 640, 120);
+      @view2 = CandyGear::View3D.new(
+        CandyGear::Vector4D.new(0, 240, 1280, 480), 1280, 480);
+      CandyGear.views = [@view1, @view2];
+
+      @view2.camera_position = @camera_position;
+      @view2.camera_rotation = @camera_rotation;
+    end
+
+    def key_down(key)
+      case key
+      when CandyGear::Key::I
+        @camera_rotation.rotate(-CAMERA_ROTATION_SPEED, 0.0);
+      when CandyGear::Key::K
+        @camera_rotation.rotate(CAMERA_ROTATION_SPEED, 0.0);
+      when CandyGear::Key::J
+        @camera_rotation.rotate(0.0, CAMERA_ROTATION_SPEED);
+      when CandyGear::Key::L
+        @camera_rotation.rotate(0.0, -CAMERA_ROTATION_SPEED);
+      when CandyGear::Key::E
+        @camera_position.translate(
+          CandyGear::Vector3D.new(
+            0.0, 0.0, -TRANSLATION_SPEED), @camera_rotation);
+      when CandyGear::Key::D
+        @camera_position.translate(
+          CandyGear::Vector3D.new(
+            0.0, 0.0, TRANSLATION_SPEED), @camera_rotation);
+      when CandyGear::Key::S
+        @camera_position.translate(
+          CandyGear::Vector3D.new(
+            -TRANSLATION_SPEED, 0.0, 0.0), @camera_rotation);
+      when CandyGear::Key::F
+        @camera_position.translate(
+          CandyGear::Vector3D.new(
+            TRANSLATION_SPEED, 0.0, 0.0), @camera_rotation);
+      end
+    end
+
+    def key_up(key)
+    end
+
+    def tick()
+      @sprite.draw(@view1, @sprite_position);
+      @japanese_text_sprite.draw(@view1, @japanese_text_position);
+      @english_text_sprite.draw(@view1, @english_text_position);
+      @instances_rotation.rotate(0.0, BOX_ROTATION_SPEED);
+      @rectangle.draw_rectangle(@view1, @color);
+      @instances.each do |i|
+        @model.draw(i, @instances_rotation);
+      end
+    end
+  end
+end
diff --git a/test/src/mode/title.rb b/test/src/mode/title.rb
new file mode 100644
index 0000000..6347b70
--- /dev/null
+++ b/test/src/mode/title.rb
@@ -0,0 +1,49 @@
+# Copyright 2022-2023 Frederico de Oliveira Linhares
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+module Mode
+  class Title
+    def initialize()
+      @view = CandyGear::View2D.new(
+        CandyGear::Vector4D.new(0, 0, 1280, 720), 640, 360);
+      CandyGear.views = [@view];
+
+      @menu = CandyGear::Menu.new(
+        @view,
+        $global_data[:menu_view], 10, 10,
+        [
+          {text: "Demo", action: -> {change_mode(:demo);}},
+          {text: "Quit", action: -> {CandyGear.quit();}}
+        ]);
+    end
+
+    def key_down(key)
+      case key
+      when CandyGear::Key::I
+        @menu.pred_opt();
+      when CandyGear::Key::K
+        @menu.next_opt();
+      when CandyGear::Key::F
+        @menu.activate();
+      end
+    end
+
+    def key_up(key)
+    end
+
+    def tick()
+      @menu.draw();
+    end
+  end
+end
diff --git a/test/textures/menu.qoi b/test/textures/menu.qoi
new file mode 100644
index 0000000..177a547
Binary files /dev/null and b/test/textures/menu.qoi differ
-- 
cgit v1.2.3