GTK Tetris + Cairo 튜토리얼용 Glimmer DSL
다음은 (기본Glimmer DSL for GTK rewriting of Mohit's tutorial 명령형 구문 대신) 훨씬 간단하고 유지 관리하기 쉬운Glimmer DSL for GTK declarative Cairo syntax을 사용하는 RCairo입니다.
GTK용 Glimmer DSL과 함께 Ruby에서 Cairo를 사용하는 튜토리얼:
Cairo은 GTK에서 임의의 2D 기하학적 모양을 그리는 엔진입니다.
Glimmer DSL for GTK 에서 SVG 작동 방식과 유사한 방식으로 Cairo 모양을 선언적으로 그릴 수 있지만 하나의 언어를 사용합니다. Ruby, 따라서 필요할 때 손쉽게 Ruby 로직(예: if 문 또는 각 루프)을 활용할 수 있습니다. 선언적 구문은 또한 더 단순하고 중첩 속성의 순서에 의존하지 않고 더 이해하기 쉽고 유지 관리하기 쉬운 코드를 생성합니다.
아래는 Mohit Sindhwani's blog post "Cairo with Ruby - Samples using RCairo" 에서 영감을 받아 이식한 샘플로 구성된 빠른 자습서입니다.
호
samples/cairo/arc.rb
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Hello, Drawing Area!'
default_size 256, 256
drawing_area {
# Surface Paint
paint 242.25, 242.25, 242.25
# Set up the parameters
xc = 128.0
yc = 128.0
radius = 100.0
angle1 = 45.0 * (Math::PI/180.0) # angles are specified
angle2 = 180.0 * (Math::PI/180.0) # in radians
# The main arc
arc(xc, yc, radius, angle1, angle2) {
stroke 0, 0, 0
line_width 10
}
# Draw helping lines
# First, the circle at the centre
arc(xc, yc, 10.0, 0, 2*Math::PI) {
fill 255, 51, 51, 0.6
}
# Then, the lines reaching out
path {
arc xc, yc, radius, angle1, angle1
line_to xc, yc
arc xc, yc, radius, angle2, angle2
line_to xc, yc
stroke 255, 51, 51, 0.6
line_width 6
}
}
}.show
아크 네거티브
samples/cairo/arc_negative.rb
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Arc Negative'
default_size 256, 256
drawing_area {
# Surface Paint
paint 255, 255, 255
# Set up the parameters
xc = 128.0
yc = 128.0
radius = 100.0
angle1 = 45.0 * (Math::PI/180.0) # angles are specified
angle2 = 180.0 * (Math::PI/180.0) # in radians
# The main negative arc
arc_negative(xc, yc, radius, angle1, angle2) {
stroke 0, 0, 0
line_width 10
}
# Draw helping lines
# First, the circle at the centre
arc(xc, yc, 10.0, 0, 2*Math::PI) {
fill 255, 51, 51, 0.6
}
# Then, the lines reaching out
path {
arc(xc, yc, radius, angle1, angle1)
line_to(xc, yc)
arc(xc, yc, radius, angle2, angle2)
line_to(xc, yc)
stroke 255, 51, 51, 0.6
line_width 6
}
}
}.show
클립
samples/cairo/clip.rb
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Clip'
default_size 256, 256
drawing_area {
# Surface Paint
paint 255, 255, 255
# Designate arc as the clipping area
arc(128.0, 128.0, 76.8, 0, 2 * Math::PI) {
clip true
}
# Rectangle will get clipped by arc
rectangle(0, 0, 256, 256) {
fill 0, 0, 0
}
# Path will get clipped by arc
path {
move_to 0, 0
line_to 256, 256
move_to 256, 0
line_to 0, 256
stroke 0, 255, 0
line_width 10
}
}
}.show
클립 이미지
samples/cairo/clip_image.rb
require 'glimmer-dsl-gtk'
require 'net/http'
image_content = Net::HTTP.get(URI('https://raw.githubusercontent.com/AndyObtiva/glimmer-dsl-gtk/master/images/breaking-blue-wave.png'))
image_file = File.join(Dir.home, 'breaking-blue-wave.png')
File.write(image_file, image_content)
include Glimmer
window {
title 'Clip Image'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
arc(128.0, 128.0, 76.8, 0, 2 * Math::PI) {
clip true # designate arc as the clipping area
}
rectangle(0, 0, 256, 256) {
# Source image is from:
# - https://www.publicdomainpictures.net/en/view-image.php?image=7683&picture=breaking-blue-wave
# Converted to PNG before using it
image = Cairo::ImageSurface.from_png(image_file)
w = image.width
h = image.height
scale 256.0/w, 256.0/h, exclude: :shape # applies scale to fill source image only
fill image, 0, 0
}
}
}.show
곡선
samples/cairo/curve_to.rb
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Curve to'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
x=25.6
y=128.0
x1=102.4
y1=230.4
x2=153.6
y2=25.6
x3=230.4
y3=128.0
path {
move_to x, y
curve_to x1, y1, x2, y2, x3, y3
line_width 10
stroke 0, 0, 0
}
path {
move_to x,y
line_to x1,y1
move_to x2,y2
line_to x3,y3
line_width 6
stroke 255, 51, 51, 0.6
}
}
}.show
대시
samples/cairo/dashes.rb
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Dashes'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
dashes = [ 50.0, # ink
10.0, # skip
10.0, # ink
10.0 # skip
]
offset = -50.0
path {
move_to 128.0, 25.6
line_to 230.4, 230.4
rel_line_to -102.4, 0.0
curve_to 51.2, 230.4, 51.2, 128.0, 128.0, 128.0
line_width 10
dash dashes, offset
stroke 0, 0, 0
}
}
}.show
채우기 및 획 2
(참고: 채우기 및 스트로크 1이 없습니다. 이것은 채우기 및 스트로크 2만 언급한 Mohit's blog post 에서 채택되었습니다.)
samples/cairo/fill_and_stroke2.rb
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Fill and Stroke 2'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
path {
move_to 128.0, 25.6
line_to 230.4, 230.4
rel_line_to -102.4, 0.0
curve_to 51.2, 230.4, 51.2, 128.0, 128.0, 128.0
close_path
fill 0, 0, 255
stroke 0, 0, 0
line_width 10
}
path {
move_to 64.0, 25.6
rel_line_to 51.2, 51.2
rel_line_to -51.2, 51.2
rel_line_to -51.2, -51.2
close_path
fill 0, 0, 255
stroke 0, 0, 0
line_width 10
}
}
}.show
채우기 스타일
samples/cairo/fill_style.rb
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Fill Style'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
path {
rectangle 12, 12, 232, 70
path { # sub-path
arc 64, 64, 40, 0, 2*Math::PI
}
path { # sub-path
arc_negative 192, 64, 40, 0, -2*Math::PI
}
fill_rule Cairo::FILL_RULE_EVEN_ODD
line_width 6
fill 0, 178.5, 0
stroke 0, 0, 0
}
path {
rectangle 12, 12, 232, 70
path { # sub-path
arc 64, 64, 40, 0, 2*Math::PI
}
path { # sub-path
arc_negative 192, 64, 40, 0, -2*Math::PI
}
translate 0, 128
fill_rule Cairo::FILL_RULE_WINDING
line_width 6
fill 0, 0, 229.5
stroke 0, 0, 0
}
}
}.show
구배
samples/cairo/gradient.rb
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Gradient'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
# Create the Linear Pattern
rectangle(0, 0, 256, 256) {
pat = Cairo::LinearPattern.new(0.0, 0.0, 0.0, 256.0)
pat.add_color_stop_rgba(1, 0, 0, 0, 1)
pat.add_color_stop_rgba(0, 1, 1, 1, 1)
fill pat
}
# Create the radial pattern
arc(128.0, 128.0, 76.8, 0, 2 * Math::PI) {
pat = Cairo::RadialPattern.new(115.2, 102.4, 25.6,
102.4, 102.4, 128.0)
pat.add_color_stop_rgba(0, 1, 1, 1, 1)
pat.add_color_stop_rgba(1, 0, 0, 0, 1)
fill pat
}
}
}.show
영상
samples/cairo/image.rb
require 'glimmer-dsl-gtk'
require 'net/http'
image_content = Net::HTTP.get(URI('https://raw.githubusercontent.com/AndyObtiva/glimmer-dsl-gtk/master/images/breaking-blue-wave.png'))
image_file = File.join(Dir.home, 'breaking-blue-wave.png')
File.write(image_file, image_content)
include Glimmer
window {
title 'Image'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
image = Cairo::ImageSurface.from_png(image_file)
w = image.width
h = image.height
translate 128.0, 128.0
rotate 45*Math::PI/180
scale 256.0/w, 256.0/h
translate -0.5*w, -0.5*h
paint image, 0, 0
}
}.show
이미지 그라데이션
samples/cairo/image_gradient.rb
require 'glimmer-dsl-gtk'
require 'net/http'
image_content = Net::HTTP.get(URI('https://raw.githubusercontent.com/AndyObtiva/glimmer-dsl-gtk/master/images/breaking-blue-wave.png'))
image_file = File.join(Dir.home, 'breaking-blue-wave.png')
File.write(image_file, image_content)
include Glimmer
window {
title 'Image Gradient'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
image = Cairo::ImageSurface.from_png(image_file)
w = image.width
h = image.height
# Load the image as a surface pattern
pattern = Cairo::SurfacePattern.new(image)
pattern.extend = Cairo::EXTEND_REPEAT
# Set up the scale matrix
pattern.matrix = Cairo::Matrix.scale(w/256.0 * 5.0, h/256.0 * 5.0)
rectangle(0, 0, 256, 256) {
translate 128.0, 128.0
rotate Math::PI / 4
scale 1/Math.sqrt(2), 1/Math.sqrt(2)
translate -128.0, -128.0
fill pattern
}
}
}.show
멀티 세그먼트 캡
samples/cairo/multi_segment_caps.rb
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Multi Segment Caps'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
path {
move_to 50.0, 75.0
line_to 200.0, 75.0
move_to 50.0, 125.0
line_to 200.0, 125.0
move_to 50.0, 175.0
line_to 200.0, 175.0
line_width 30
line_cap Cairo::LINE_CAP_ROUND
stroke 0, 0, 0
}
}
}.show
둥근 직사각형
samples/cairo/rounded_rectangle.rb
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Rounded Rectangle'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
path {
rounded_rectangle(25.6, 25.6, 204.8, 204.8, 20)
fill 127.5, 127.5, 255
line_width 10.0
stroke 127.5, 0, 0, 0.5
}
}
}.show
라인 캡 설정
samples/cairo/set_line_cap.rb
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Set line cap'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
# The main code
path {
move_to 64.0, 50.0
line_to 64.0, 200.0
line_cap Cairo::LINE_CAP_BUTT # default
line_width 30
stroke 0, 0, 0
}
path {
move_to 128.0, 50.0
line_to 128.0, 200.0
line_cap Cairo::LINE_CAP_ROUND
line_width 30
stroke 0, 0, 0
}
path {
move_to 192.0, 50.0
line_to 192.0, 200.0
line_cap Cairo::LINE_CAP_SQUARE
line_width 30
stroke 0, 0, 0
}
# draw helping lines */
path {
move_to 64.0, 50.0
line_to 64.0, 200.0
move_to 128.0, 50.0
line_to 128.0, 200.0
move_to 192.0, 50.0
line_to 192.0, 200.0
line_width 2.56
stroke 255, 51, 51
}
}
}.show
라인 조인 설정
samples/cairo/set_line_join.rb
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Set line join'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
# The main code
path {
move_to 76.8, 84.48
rel_line_to 51.2, -51.2
rel_line_to 51.2, 51.2
line_join Cairo::LINE_JOIN_MITER # default
line_width 40.96
stroke 0, 0, 0
}
path {
move_to 76.8, 161.28
rel_line_to 51.2, -51.2
rel_line_to 51.2, 51.2
line_join Cairo::LINE_JOIN_BEVEL
line_width 40.96
stroke 0, 0, 0
}
path {
move_to 76.8, 238.08
rel_line_to 51.2, -51.2
rel_line_to 51.2, 51.2
line_join Cairo::LINE_JOIN_ROUND
line_width 40.96
stroke 0, 0, 0
}
}
}.show
텍스트
samples/cairo/text.rb
require 'glimmer-dsl-gtk'
include Glimmer
window {
title 'Text'
default_size 256, 256
drawing_area {
paint 242.25, 242.25, 242.25
font_family = OS.linux? ? 'Sans' : (OS.mac? ? 'Helvetica' : 'Arial')
# The main code
path {
move_to 10.0, 135.0
show_text 'Hello'
font_face font_family, Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_BOLD
font_size 90.0
line_width 2.56
fill 0, 0, 0
stroke 0, 0, 0
}
path {
move_to 70.0, 165.0
text_path 'void'
font_face font_family, Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_BOLD
font_size 90.0
line_width 2.56
fill 127.5, 127.5, 255
stroke 0, 0, 0
}
# draw helping lines
path {
arc 10.0, 135.0, 5.12, 0, 2*Math::PI
close_path
arc 70.0, 165.0, 5.12, 0, 2*Math::PI
fill 255, 51, 51, 0.6
}
}
}.show
테트리스 스크린샷:
다음은 Tetris 으로 작성된 declarative Cairo code using Glimmer DSL for GTK 의 스크린샷입니다.
GTK 코드용 Tetris Glimmer DSL:
카이로 사용은 대부분 Glimmer-only
square
및 polygon
모양 구조입니다(표준RCairo에서는 사용할 수 없음).# From: https://github.com/AndyObtiva/glimmer-dsl-gtk#tetris
require 'glimmer-dsl-gtk'
require_relative 'tetris/model/game'
class Tetris
include Glimmer
BLOCK_SIZE = 25
BEVEL_CONSTANT = 20
COLOR_GRAY = [192, 192, 192]
def initialize
@game = Model::Game.new
end
def launch
create_gui
register_observers
@game.start!
@main_window.show
end
def create_gui
@main_window = window {
title 'Glimmer Tetris'
default_size Model::Game::PLAYFIELD_WIDTH * BLOCK_SIZE, Model::Game::PLAYFIELD_HEIGHT * BLOCK_SIZE # + 98
box(:vertical) {
tetris_menu_bar
box(:horizontal) {
@playfield_blocks = playfield(playfield_width: @game.playfield_width, playfield_height: @game.playfield_height, block_size: BLOCK_SIZE)
score_board
}
}
on(:key_press_event) do |widget, key_event|
case key_event.keyval
when 65364 # down arrow
@game.down!
when 32 # space
@game.down!(instant: true)
when 65362 # up arrow
case @game.up_arrow_action
when :instant_down
@game.down!(instant: true)
when :rotate_right
@game.rotate!(:right)
when :rotate_left
@game.rotate!(:left)
end
when 65361 # left arrow
@game.left!
when 65363 # right arrow
@game.right!
when 65506 # right shift
@game.rotate!(:right)
when 65505 # left shift
@game.rotate!(:left)
else
# Do Nothing
end
end
}
end
def register_observers
observe(@game, :game_over) do |game_over|
if game_over
show_game_over_dialog
else
start_moving_tetrominos_down
end
end
@game.playfield_height.times do |row|
@game.playfield_width.times do |column|
observe(@game.playfield[row][column], :color) do |new_color|
color = new_color
block = @playfield_blocks[row][column]
block[:background_square].fill = color
block[:top_bevel_edge].fill = [color[0] + 4*BEVEL_CONSTANT, color[1] + 4*BEVEL_CONSTANT, color[2] + 4*BEVEL_CONSTANT]
block[:right_bevel_edge].fill = [color[0] - BEVEL_CONSTANT, color[1] - BEVEL_CONSTANT, color[2] - BEVEL_CONSTANT]
block[:bottom_bevel_edge].fill = [color[0] - BEVEL_CONSTANT, color[1] - BEVEL_CONSTANT, color[2] - BEVEL_CONSTANT]
block[:left_bevel_edge].fill = [color[0] - BEVEL_CONSTANT, color[1] - BEVEL_CONSTANT, color[2] - BEVEL_CONSTANT]
block[:border_square].stroke = new_color == Model::Block::COLOR_CLEAR ? COLOR_GRAY : color
block[:drawing_area].queue_draw
false
end
end
end
Model::Game::PREVIEW_PLAYFIELD_HEIGHT.times do |row|
Model::Game::PREVIEW_PLAYFIELD_WIDTH.times do |column|
observe(@game.preview_playfield[row][column], :color) do |new_color|
color = new_color
block = @preview_playfield_blocks[row][column]
block[:background_square].fill = color
block[:top_bevel_edge].fill = [color[0] + 4*BEVEL_CONSTANT, color[1] + 4*BEVEL_CONSTANT, color[2] + 4*BEVEL_CONSTANT]
block[:right_bevel_edge].fill = [color[0] - BEVEL_CONSTANT, color[1] - BEVEL_CONSTANT, color[2] - BEVEL_CONSTANT]
block[:bottom_bevel_edge].fill = [color[0] - BEVEL_CONSTANT, color[1] - BEVEL_CONSTANT, color[2] - BEVEL_CONSTANT]
block[:left_bevel_edge].fill = [color[0] - BEVEL_CONSTANT, color[1] - BEVEL_CONSTANT, color[2] - BEVEL_CONSTANT]
block[:border_square].stroke = new_color == Model::Block::COLOR_CLEAR ? COLOR_GRAY : color
block[:drawing_area].queue_draw
end
end
end
observe(@game, :score) do |new_score|
@score_label.text = new_score.to_s
end
observe(@game, :lines) do |new_lines|
@lines_label.text = new_lines.to_s
end
observe(@game, :level) do |new_level|
@level_label.text = new_level.to_s
end
end
def tetris_menu_bar
menu_bar {
menu_item(label: 'Game') { |mi|
m = menu {
check_menu_item(label: 'Pause') {
on(:activate) do
@game.paused = !@game.paused?
end
}
menu_item(label: 'Restart') {
on(:activate) do
@game.restart!
end
}
separator_menu_item
menu_item(label: 'Exit') {
on(:activate) do
@main_window.close
end
}
}
mi.submenu = m.gtk
}
menu_item(label: 'View') { |mi|
m = menu {
menu_item(label: 'Show High Scores') {
on(:activate) do
show_high_score_dialog
end
}
menu_item(label: 'Clear High Scores') {
on(:activate) do
@game.clear_high_scores!
end
}
}
mi.submenu = m.gtk
}
menu_item(label: 'Options') { |mi|
m = menu {
rmi = radio_menu_item(nil, 'Instant Down on Up') {
on(:activate) do
@game.instant_down_on_up!
end
}
default_rmi = radio_menu_item(rmi.group, 'Rotate Right on Up') {
on(:activate) do
@game.rotate_right_on_up!
end
}
default_rmi.activate
radio_menu_item(rmi.group, 'Rotate Left on Up') {
on(:activate) do
@game.rotate_left_on_up!
end
}
}
mi.submenu = m.gtk
}
menu_item(label: 'Options') { |mi|
m = menu {
menu_item(label: 'About') {
on(:activate) do
show_about_dialog
end
}
}
mi.submenu = m.gtk
}
}
end
def score_board
box(:vertical) {
label
@preview_playfield_blocks = playfield(playfield_width: Model::Game::PREVIEW_PLAYFIELD_WIDTH, playfield_height: Model::Game::PREVIEW_PLAYFIELD_HEIGHT, block_size: BLOCK_SIZE)
label
label('Score')
@score_label = label
label
label('Lines')
@lines_label = label
label
label('Level')
@level_label = label
label
}
end
def playfield(playfield_width: , playfield_height: , block_size: , &extra_content)
blocks = []
box(:vertical) {
playfield_height.times.map do |row|
blocks << []
box(:horizontal) {
playfield_width.times.map do |column|
blocks.last << block(row: row, column: column, block_size: block_size)
end
}
end
extra_content&.call
}
blocks
end
def block(row: , column: , block_size: , &extra_content)
block = {}
bevel_pixel_size = 0.16 * block_size.to_f
color = Model::Block::COLOR_CLEAR
block[:drawing_area] = drawing_area {
size_request block_size, block_size
block[:background_square] = square(0, 0, block_size) {
fill *color
}
block[:top_bevel_edge] = polygon(0, 0, block_size, 0, block_size - bevel_pixel_size, bevel_pixel_size, bevel_pixel_size, bevel_pixel_size) {
fill color[0] + 4*BEVEL_CONSTANT, color[1] + 4*BEVEL_CONSTANT, color[2] + 4*BEVEL_CONSTANT
}
block[:right_bevel_edge] = polygon(block_size, 0, block_size - bevel_pixel_size, bevel_pixel_size, block_size - bevel_pixel_size, block_size - bevel_pixel_size, block_size, block_size) {
fill color[0] - BEVEL_CONSTANT, color[1] - BEVEL_CONSTANT, color[2] - BEVEL_CONSTANT
}
block[:bottom_bevel_edge] = polygon(block_size, block_size, 0, block_size, bevel_pixel_size, block_size - bevel_pixel_size, block_size - bevel_pixel_size, block_size - bevel_pixel_size) {
fill color[0] - BEVEL_CONSTANT, color[1] - BEVEL_CONSTANT, color[2] - BEVEL_CONSTANT
}
block[:left_bevel_edge] = polygon(0, 0, 0, block_size, bevel_pixel_size, block_size - bevel_pixel_size, bevel_pixel_size, bevel_pixel_size) {
fill color[0] - BEVEL_CONSTANT, color[1] - BEVEL_CONSTANT, color[2] - BEVEL_CONSTANT
}
block[:border_square] = square(0, 0, block_size) {
stroke *COLOR_GRAY
}
extra_content&.call
}
block
end
def start_moving_tetrominos_down
unless @tetrominos_start_moving_down
@tetrominos_start_moving_down = true
GLib::Timeout.add(@game.delay*1000) do
@game.down! if !@game.game_over? && !@game.paused?
true
end
end
end
def show_game_over_dialog
message_dialog(@main_window) { |md|
title 'Game Over!'
text "Score: #{@game.high_scores.first.score}\nLines: #{@game.high_scores.first.lines}\nLevel: #{@game.high_scores.first.level}"
on(:response) do
md.destroy
end
}.show
@game.restart!
false
end
def show_high_score_dialog
game_paused = !!@game.paused
@game.paused = true
if @game.high_scores.empty?
high_scores_string = "No games have been scored yet."
else
high_scores_string = @game.high_scores.map do |high_score|
"#{high_score.name} | Score: #{high_score.score} | Lines: #{high_score.lines} | Level: #{high_score.level}"
end.join("\n")
end
message_dialog(@main_window) { |md|
title 'High Scores'
text high_scores_string
on(:response) do
md.destroy
end
}.show
@game.paused = game_paused
end
def show_about_dialog
message_dialog(@main_window) { |md|
title 'About'
text "Glimmer Tetris\n\nGlimmer DSL for GTK\n\nElaborate Sample\n\nCopyright (c) 2021-2022 Andy Maleh"
on(:response) do
md.destroy
end
}.show
end
end
Tetris.new.launch
해피Glimmering !
Reference
이 문제에 관하여(GTK Tetris + Cairo 튜토리얼용 Glimmer DSL), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/andyobtiva/glimmer-dsl-for-gtk-tetris-cairo-tutorial-inspired-by-mohit-sindhwani-4e30텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)