AndyObtiva / glimmer-dsl-libui

Glimmer DSL for LibUI - Prerequisite-Free Ruby Desktop Development Cross-Platform Native GUI Library - The Quickest Way From Zero To GUI - If You Liked Shoes, You'll Love Glimmer! - No need to pre-install any prerequisites. Just install the gem and have platform-independent GUI that just works on Mac, Windows, and Linux.
MIT License
497 stars 15 forks source link

Cannot update fill of a circle? #49

Closed phuongnd08 closed 1 year ago

phuongnd08 commented 1 year ago

Using this:

    color_indicator =  circle(200, 200, 90) { # declarative stable path (implicit path syntax for a single shape nested directly under area)
      stroke r: 0, g: 0, b: 0, thickness: 2
    }

    controller.on_stat_updated do |controller|
      puts "fill with color = #{controller.color}"
      color_indicator.fill = controller.color
    end

when controller change the color to :green, :red, :yellow, I noticed that the color of the circle is not updated. What am I supposed to do so that the color changes?

AndyObtiva commented 1 year ago

That depends on whether you're using stable paths by nesting a circle directly under an area or by using redrawable dynamic paths by using an on_draw listener under area and then nesting a circle within it (you'd have to share the full code of your file to make it clear to me which approach you follow unless the solution below works).

In the case of a stable path (the simpler approach), you don't have to do anything other than change the fill color of the circle object.

But, in the case of a dynamic path (the more complicated approach, albeit slightly more performant), you'd have to call the area.queue_redraw_all method after every change you make to paths (shapes) nested underneath the area's on_draw listener:

area_object.queue_redraw_all

This should address your concern fully, so I am closing this issue. But, you can still reply to the issue if you want to confirm if the solution works or ask further questions (or even re-open the issue if needed).

Cheers.

phuongnd08 commented 1 year ago

This is how I do it:

controller = GameController.new
controller.run_game_in_thread

window('hello world'){
  fullscreen true
  scrolling_area {
    text {
      indicator = string

      controller.on_stat_updated do |controller|
        indicator.string = controller.indication
      end
    }
    color_indicator =  circle(200, 200, 90) { # declarative stable path (implicit path syntax for a single shape nested directly under area)
      stroke r: 0, g: 0, b: 0, thickness: 2
      fill controller.color
    }

    controller.on_stat_updated do |controller|
      puts "fill with color = #{controller.color}"
      color_indicator.fill = controller.color
    end

controller is a model object, with the ability to subscribe to some of it changes

class GameController
...
  def on_stat_updated(&block)
    @callbacks << block
    notify_callbacks
  end

  def notify_callbacks
    @callbacks.each { |callback| callback.call(self) }
  end
end
phuongnd08 commented 1 year ago

Tried this, still no color update

  game_area = scrolling_area {
    text {
      indicator = string

      controller.on_stat_updated do |controller|
        indicator.string = controller.indication
      end
    }
    color_indicator =  circle(200, 200, 90) { # declarative stable path (implicit path syntax for a single shape nested directly under area)
      stroke r: 0, g: 0, b: 0, thickness: 2
      fill controller.color
    }

    controller.on_stat_updated do |controller|
      puts "fill with color = #{controller.color}"
      color_indicator.fill = controller.color
      game_area&.queue_redraw_all
    end
phuongnd08 commented 1 year ago

Tried this too, no color update:

    color_indicator = nil
    color_area = area {
      color_indicator =  circle(200, 200, 90) { # declarative stable path (implicit path syntax for a single shape nested directly under area)
        stroke r: 0, g: 0, b: 0, thickness: 2
        fill <= controller.color
      }
    }

    controller.on_stat_updated do |controller|
      puts "fill with color = #{controller.color}"
      color_indicator.fill = controller.color
      color_area&.queue_redraw_all
    end
phuongnd08 commented 1 year ago

Whole file incase you want to quickly debug:

#!/usr/bin/env ruby

require 'glimmer-dsl-libui'
require 'byebug'
require 'observer'
require 'shellwords'
require 'timeout'
require 'mpv'

include Glimmer

class AudioPlayer
  def initialize(path)
    @path = path
  end

  def play
    session.command "loadfile", @path
  end

  def mpv_args
    [
      "--profile=low-latency",
    ]
  end

  def session
    @session ||= MPV::Session.new(user_args: mpv_args).tap do |s|# contains both a MPV::Server and a MPV::Client
      s.callbacks << method(:event_happened)
    end
  end

  def event_happened(event)
    case event["event"]
    when "pause"
      @paused = true
    when "unpause"
      @paused = false
    end
  end
end

class GameController
  attr_accessor :total_counter, :correct_counter, :game_session_started
  attr_accessor :current_char
  attr_reader :color

  def initialize
    @total_counter = 0
    @correct_counter = 0
    @game_session_started = false
    @callbacks = []
    @color = :black
  end

  def play_word(word)
    @players ||= {}
    @players[word] ||=
      begin
        path = File.expand_path("wsound/#{word}.aiff", __dir__)
        unless File.exists?(path)
          system "say #{Shellwords.escape(word)} -o #{Shellwords.escape(path)}"
        end

        AudioPlayer.new(path)
      end

    @players[word].play
    self.color = word.downcase.to_sym
  end

  def color=(c)
    @color = c
    notify_callbacks
  end

  def on_stat_updated(&block)
    @callbacks << block
    notify_callbacks
  end

  def notify_callbacks
    @callbacks.each { |callback| callback.call(self) }
  end

  def check_answer(char)
    puts "user_answer=#{char}"
    @user_answer = char
    @user_answered_at = Time.now
  end

  def clear_answer
    @user_answer = nil
    @user_answered_at = nil
  end

  def toggle_game_mode!
    @game_session_started = !@game_session_started
  end

  def run_game_in_thread
    Thread.new { run_game }
  end

  GAME_KEYS = {
    "a" => "red",
    "b" => "green",
    "c" => "yellow"
  }

  def duration_per_game
    3
  end

  def run_game
    loop do
      unless @game_session_started
        sleep 0.1
        next
      end

      puts "new game"
      self.current_char = GAME_KEYS.keys.sample
      self.total_counter += 1
      announce_target(GAME_KEYS[current_char])
      clear_answer
      @game_started_at = Time.now
      outcome = nil
      perform_for(duration: duration_per_game, interval: 0.05) do
        unless @user_answer.nil?
          answwer_time = @user_answered_at - @game_started_at
          puts "answer-time=#{(answwer_time * 1000).floor(1)} ms"
          if answwer_time > duration_per_game
            outcome = :timeout
          elsif @user_answer == self.current_char
            outcome = :right
          else
            outcome = :wrong
          end
          next true
        end
      end

      outcome ||= :timeout

      if outcome == :right
        self.correct_counter += 1
        notify_callbacks
      end

      announce_outcome(outcome)
    end
  rescue => e
    p e
  end

  def perform_for(duration:, interval: 0.05)
    started_at = Time.now
    while Time.now - started_at < duration
      sleep interval
      break if yield
    end
  end

  def announce_outcome(outcome)
    play_sound(outcome.to_s)
  end

  def announce_target(word)
    say("ready")
    sleep 0.2
    say("3")
    sleep 0.2
    say("2")
    sleep 0.2
    say("1")
    play_word(word)
  end

  def say(msg, voice: nil)
    Timeout.timeout(3) do
      cmd = "say #{Shellwords.escape(msg)}"
      cmd += " -v #{Shellwords.escape(voice)}" if voice
      system cmd
      puts "said: #{msg}"
    end
  rescue Timeout::Error => e
    puts "Timeout error while speaking #{msg} using voice=#{voice}"
  end

  def play_sound(sound)
    path = File.expand_path("sound/#{sound}.mp3", __dir__)
    system "afplay #{Shellwords.escape(path)}"
  end

  def indication
    if @game_session_started
      "#{correct_counter}/#{total_counter}"
    else
      "Press ENTER to start/stop game"
    end
  end
end

controller = GameController.new
controller.run_game_in_thread

window('hello world'){
  fullscreen true
  scrolling_area {
    text {
      indicator = string

      controller.on_stat_updated do |controller|
        indicator.string = controller.indication
      end
    }

    color_indicator = nil
    color_area = area {
      color_indicator =  circle(200, 200, 90) { # declarative stable path (implicit path syntax for a single shape nested directly under area)
        stroke r: 0, g: 0, b: 0, thickness: 2
        fill <= controller.color
      }
    }

    controller.on_stat_updated do |controller|
      puts "fill with color = #{controller.color}"
      color_indicator.fill = controller.color
      color_area&.queue_redraw_all
    end

    on_mouse_down do |mouse_event|
      if mouse_event[:down] == 1
        controller.check_answer("c")
      elsif mouse_event[:down] == 3
        controller.check_answer("d")
      end
    end

    on_key_down do |key_event|
      # if key_event[:ext_key] == :escape
      #   LibUI.quit
      # else
      case key_event[:key]
      when "a", "b" then controller.check_answer(key_event[:key])
      when "\n" then controller.toggle_game_mode!
      end
      # end

      nil
    end
  }
}.show

And the Gemfile

# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem 'glimmer-dsl-libui'
gem 'byebug'
gem 'dotenv'
gem 'mpv'
phuongnd08 commented 1 year ago

@AndyObtiva tagging you just to be sure

AndyObtiva commented 1 year ago

I replied in the follow-up issue you created: https://github.com/AndyObtiva/glimmer-dsl-libui/issues/50

Sorry, it seems only I have the right to re-open an issue. I would have re-opened it had you not created the other issue. No worries, let's just continue communication over there.