A Ruby desktop application developed with Glimmer DSL for LibUI as part of the Montreal.rb September 2024 meetup, titled "Ruby GUI Desktop Development Hands-On Tutorial".
- Todo MVC Glimmer DSL for LibUI Desktop Edition
- Steps
- Step 1 - Scaffold Application
- Step 2 - Add Todos Table with Fake Data
- Step 3 - Add New Todo Entry
- Step 4 - Add Delete Todo Button
- Step 5 - Add Completed Table Column
- Step 6 - Add Toggle All Button
- Step 7 - Add Items Left Label
- Step 8 - Add All/Active/Completed Buttons
- Step 9 - Add Clear Completed Button
- Step 10 - Refactor To Components
- Steps
Ensure your git user.name
is configured.
git config --global user.name "FirstName LastName"
Ensure your git github.user
is configured.
git config --global github.user githubusername
Scaffold application by running terminal command:
glimmer "scaffold[todo_mvc]"
Application is scaffolded in todo_mvc
directory.
Enter application directory by running terminal command:
cd todo_mvc
Run application by running terminal command:
glimmer run
or
bin/todo_mvc
Delete Greeting
Model by running terminal command:
rm app/todo_mvc/model/greeting.rb
Create Todo
Model by running terminal command:
touch app/todo_mvc/model/todo.rb
Add the following code inside app/todo_mvc/model/todo.rb
:
class TodoMvc
module Model
class Todo
attr_accessor :task
def initialize(task)
@task = task
end
end
end
end
Create TodoList
Model by running terminal command:
touch app/todo_mvc/model/todo_list.rb
Add the following code inside app/todo_mvc/model/todo_list.rb
:
require 'todo_mvc/model/todo'
class TodoMvc
module Model
class TodoList
attr_accessor :todos
def initialize
@todos = []
end
def add_todo(task)
todos << Todo.new(task)
end
end
end
end
Replace the content of app/todo_mvc/view/todo_mvc.rb
with the following code:
require 'todo_mvc/model/todo_list'
class TodoMvc
module View
class TodoMvc
include Glimmer::LibUI::Application
before_body do
@todo_list = Model::TodoList.new
['Home Improvement', 'Shopping', 'Cleaning'].each do |task|
@todo_list.add_todo(task)
end
end
body {
window {
title 'Todo MVC'
content_size 480, 480
margined true
table {
text_column('Task')
cell_rows <=> [@todo_list, :todos]
}
}
}
end
end
end
Run application by running terminal command:
glimmer run
Replace the content of app/todo_mvc/view/todo_mvc.rb
with the following code:
require 'todo_mvc/model/todo_list'
class TodoMvc
module View
class TodoMvc
include Glimmer::LibUI::Application
before_body do
@todo_list = Model::TodoList.new
['Home Improvement', 'Shopping', 'Cleaning'].each do |task|
@todo_list.add_todo(task)
end
end
body {
window {
title 'Todo MVC'
content_size 480, 480
margined true
vertical_box {
horizontal_box {
stretchy false
entry {
text <=> [@todo_list.new_todo, :task]
}
button('Add') {
stretchy false
on_clicked do
@todo_list.add_todo
end
}
}
table {
text_column('Task')
cell_rows <=> [@todo_list, :todos]
}
}
}
}
end
end
end
Replace the content of app/todo_mvc/model/todo_list.rb
with the following code:
require 'todo_mvc/model/todo'
class TodoMvc
module Model
class TodoList
attr_accessor :todos
def initialize
@todos = []
end
def add_todo(task = nil)
task ||= new_todo.task
todos << Todo.new(task)
new_todo.task = ''
end
def new_todo
@new_todo ||= Todo.new('')
end
end
end
end
Run application by running terminal command:
glimmer run
Replace the content of app/todo_mvc/view/todo_mvc.rb
with the following code:
require 'todo_mvc/model/todo_list'
class TodoMvc
module View
class TodoMvc
include Glimmer::LibUI::Application
before_body do
@todo_list = Model::TodoList.new
['Home Improvement', 'Shopping', 'Cleaning'].each do |task|
@todo_list.add_todo(task)
end
end
body {
window {
title 'Todo MVC'
content_size 480, 480
margined true
vertical_box {
horizontal_box {
stretchy false
entry {
text <=> [@todo_list.new_todo, :task]
}
button('Add') {
stretchy false
on_clicked do
@todo_list.add_todo
end
}
}
table {
text_column('Task')
cell_rows <=> [@todo_list, :todos]
selection <=> [@todo_list, :selection_index]
}
horizontal_box {
stretchy false
button('Delete') {
stretchy false
enabled <= [@todo_list, :selection_index, on_read: -> (value) { !!value }]
on_clicked do
@todo_list.delete_todo
end
}
}
}
}
}
end
end
end
Replace the content of app/todo_mvc/model/todo_list.rb
with the following code:
require 'todo_mvc/model/todo'
class TodoMvc
module Model
class TodoList
attr_accessor :todos, :selection_index
def initialize
@todos = []
end
def add_todo(task = nil)
task ||= new_todo.task
todos << Todo.new(task)
new_todo.task = ''
end
def new_todo
@new_todo ||= Todo.new('')
end
def delete_todo
@todos.delete_at(selection_index)
end
end
end
end
Run application by running terminal command:
glimmer run
Replace the content of app/todo_mvc/view/todo_mvc.rb
with the following code:
require 'todo_mvc/model/todo_list'
class TodoMvc
module View
class TodoMvc
include Glimmer::LibUI::Application
before_body do
@todo_list = Model::TodoList.new
['Home Improvement', 'Shopping', 'Cleaning'].each do |task|
@todo_list.add_todo(task)
end
end
body {
window {
title 'Todo MVC'
content_size 480, 480
margined true
vertical_box {
horizontal_box {
stretchy false
entry {
text <=> [@todo_list.new_todo, :task]
}
button('Add') {
stretchy false
on_clicked do
@todo_list.add_todo
end
}
}
table {
checkbox_column('Completed') {
editable true
}
text_column('Task')
cell_rows <=> [@todo_list, :todos]
selection <=> [@todo_list, :selection_index]
}
horizontal_box {
stretchy false
button('Delete') {
stretchy false
enabled <= [@todo_list, :selection_index, on_read: -> (value) { !!value }]
on_clicked do
@todo_list.delete_todo
end
}
}
}
}
}
end
end
end
Replace the content of app/todo_mvc/model/todo.rb
with the following code:
class TodoMvc
module Model
class Todo
attr_accessor :task, :completed
def initialize(task)
@task = task
end
end
end
end
Run application by running terminal command:
glimmer run
Replace the content of app/todo_mvc/view/todo_mvc.rb
with the following code:
require 'todo_mvc/model/todo_list'
class TodoMvc
module View
class TodoMvc
include Glimmer::LibUI::Application
before_body do
@todo_list = Model::TodoList.new
['Home Improvement', 'Shopping', 'Cleaning'].each do |task|
@todo_list.add_todo(task)
end
end
body {
window {
title 'Todo MVC'
content_size 480, 480
margined true
vertical_box {
horizontal_box {
stretchy false
entry {
text <=> [@todo_list.new_todo, :task]
}
button('Add') {
stretchy false
on_clicked do
@todo_list.add_todo
end
}
}
horizontal_box {
stretchy false
button('Toggle All') {
stretchy false
on_clicked do
@todo_list.toggle_completion_of_all_todos
end
}
}
table {
checkbox_column('Completed') {
editable true
}
text_column('Task')
cell_rows <=> [@todo_list, :todos]
selection <=> [@todo_list, :selection_index]
}
horizontal_box {
stretchy false
button('Delete') {
stretchy false
enabled <= [@todo_list, :selection_index, on_read: -> (value) { !!value }]
on_clicked do
@todo_list.delete_todo
end
}
}
}
}
}
end
end
end
Replace the content of app/todo_mvc/model/todo.rb
with the following code:
class TodoMvc
module Model
class Todo
attr_accessor :task, :completed
def initialize(task)
@task = task
end
def active
!completed
end
def mark_completed
self.completed = true
end
def mark_active
self.completed = false
end
end
end
end
Replace the content of app/todo_mvc/model/todo_list.rb
with the following code:
require 'todo_mvc/model/todo'
class TodoMvc
module Model
class TodoList
attr_accessor :todos, :selection_index
def initialize
@todos = []
end
def add_todo(task = nil)
task ||= new_todo.task
todos << Todo.new(task)
new_todo.task = ''
end
def new_todo
@new_todo ||= Todo.new('')
end
def delete_todo
@todos.delete_at(selection_index)
end
def toggle_completion_of_all_todos
if @todos.any?(&:active)
@todos.select(&:active).each(&:mark_completed)
else
@todos.select(&:completed).each(&:mark_active)
end
end
end
end
end
Run application by running terminal command:
glimmer run
Replace the content of app/todo_mvc/view/todo_mvc.rb
with the following code:
require 'todo_mvc/model/todo_list'
class TodoMvc
module View
class TodoMvc
include Glimmer::LibUI::Application
before_body do
@todo_list = Model::TodoList.new
['Home Improvement', 'Shopping', 'Cleaning'].each do |task|
@todo_list.add_todo(task)
end
end
body {
window {
title 'Todo MVC'
content_size 480, 480
margined true
vertical_box {
horizontal_box {
stretchy false
entry {
text <=> [@todo_list.new_todo, :task]
}
button('Add') {
stretchy false
on_clicked do
@todo_list.add_todo
end
}
}
horizontal_box {
stretchy false
button('Toggle All') {
stretchy false
on_clicked do
@todo_list.toggle_completion_of_all_todos
end
}
}
table {
checkbox_column('Completed') {
editable true
}
text_column('Task')
cell_rows <=> [@todo_list, :todos]
selection <=> [@todo_list, :selection_index]
}
horizontal_box {
stretchy false
button('Delete') {
stretchy false
enabled <= [@todo_list, :selection_index, on_read: -> (value) { !!value }]
on_clicked do
@todo_list.delete_todo
end
}
}
horizontal_box {
stretchy false
label {
stretchy false
text <= [@todo_list, :active_todos,
on_read: -> (todos) { "#{todos.count} item#{'s' if todos.size != 1} left" }
]
}
}
}
}
}
end
end
end
Replace the content of app/todo_mvc/model/todo.rb
with the following code:
class TodoMvc
module Model
class Todo
attr_accessor :task, :completed
def initialize(task, todo_list: nil)
@task = task
@todo_list = todo_list
end
def completed=(value)
@completed = value
@todo_list&.recalculate_active_todos
end
def active
!completed
end
def mark_completed
self.completed = true
end
def mark_active
self.completed = false
end
end
end
end
Replace the content of app/todo_mvc/model/todo_list.rb
with the following code:
require 'todo_mvc/model/todo'
class TodoMvc
module Model
class TodoList
attr_accessor :todos, :active_todos, :selection_index
def initialize
@todos = []
@active_todos = []
end
def add_todo(task = nil)
task ||= new_todo.task
todo = Todo.new(task, todo_list: self)
todos << todo
recalculate_active_todos
new_todo.task = ''
end
def new_todo
@new_todo ||= Todo.new('')
end
def delete_todo
@todos.delete_at(selection_index)
recalculate_active_todos
end
def toggle_completion_of_all_todos
if @todos.any?(&:active)
@todos.select(&:active).each(&:mark_completed)
else
@todos.select(&:completed).each(&:mark_active)
end
end
def recalculate_active_todos
self.active_todos = @todos.select(&:active)
end
end
end
end
Run application by running terminal command:
glimmer run
Replace the content of app/todo_mvc/view/todo_mvc.rb
with the following code:
require 'todo_mvc/model/todo_list'
class TodoMvc
module View
class TodoMvc
include Glimmer::LibUI::Application
before_body do
@todo_list = Model::TodoList.new
['Home Improvement', 'Shopping', 'Cleaning'].each do |task|
@todo_list.add_todo(task)
end
end
body {
window {
title 'Todo MVC'
content_size 480, 480
margined true
vertical_box {
horizontal_box {
stretchy false
entry {
text <=> [@todo_list.new_todo, :task]
}
button('Add') {
stretchy false
on_clicked do
@todo_list.add_todo
end
}
}
horizontal_box {
stretchy false
button('Toggle All') {
stretchy false
on_clicked do
@todo_list.toggle_completion_of_all_todos
end
}
}
table {
checkbox_column('Completed') {
editable true
}
text_column('Task')
cell_rows <=> [@todo_list, :displayed_todos]
selection <=> [@todo_list, :selection_index]
}
horizontal_box {
stretchy false
button('Delete') {
stretchy false
enabled <= [@todo_list, :selection_index, on_read: -> (value) { !!value }]
on_clicked do
@todo_list.delete_todo
end
}
}
horizontal_box {
stretchy false
label {
stretchy false
text <= [@todo_list, :active_todos,
on_read: -> (todos) { "#{todos.count} item#{'s' if todos.size != 1} left" }
]
}
button('All') {
stretchy false
enabled <= [@todo_list, :filter, on_read: -> (value) { value != :all }]
on_clicked do
@todo_list.filter = :all
end
}
button('Active') {
stretchy false
enabled <= [@todo_list, :filter, on_read: -> (value) { value != :active }]
on_clicked do
@todo_list.filter = :active
end
}
button('Completed') {
stretchy false
enabled <= [@todo_list, :filter, on_read: -> (value) { value != :completed }]
on_clicked do
@todo_list.filter = :completed
end
}
}
}
}
}
end
end
end
Replace the content of app/todo_mvc/model/todo.rb
with the following code:
class TodoMvc
module Model
class Todo
attr_accessor :task, :completed
def initialize(task, todo_list: nil)
@task = task
@todo_list = todo_list
end
def completed=(value)
@completed = value
@todo_list&.recalculate_filtered_todos
end
def active
!completed
end
def mark_completed
self.completed = true
end
def mark_active
self.completed = false
end
end
end
end
Replace the content of app/todo_mvc/model/todo_list.rb
with the following code:
require 'todo_mvc/model/todo'
class TodoMvc
module Model
class TodoList
attr_accessor :todos, :active_todos, :completed_todos, :displayed_todos, :selection_index, :filter
def initialize
@todos = []
@active_todos = []
@completed_todos = []
@displayed_todos = @todos
@filter = :all
end
def add_todo(task = nil)
task ||= new_todo.task
todo = Todo.new(task, todo_list: self)
todos << todo
recalculate_filtered_todos
new_todo.task = ''
end
def new_todo
@new_todo ||= Todo.new('')
end
def delete_todo
@todos.delete_at(selection_index)
recalculate_filtered_todos
end
def toggle_completion_of_all_todos
if @todos.any?(&:active)
@todos.select(&:active).each(&:mark_completed)
else
@todos.select(&:completed).each(&:mark_active)
end
end
def recalculate_filtered_todos
self.completed_todos = @todos.select(&:completed)
self.active_todos = @todos.select(&:active)
recalculate_displayed_todos
end
def filter=(filter_value)
@filter = filter_value
recalculate_displayed_todos
end
def recalculate_displayed_todos
case filter
when :all
self.displayed_todos = todos
when :active
self.displayed_todos = active_todos
when :completed
self.displayed_todos = completed_todos
end
end
end
end
end
Run application by running terminal command:
glimmer run
Replace the content of app/todo_mvc/view/todo_mvc.rb
with the following code:
require 'todo_mvc/model/todo_list'
class TodoMvc
module View
class TodoMvc
include Glimmer::LibUI::Application
before_body do
@todo_list = Model::TodoList.new
['Home Improvement', 'Shopping', 'Cleaning'].each do |task|
@todo_list.add_todo(task)
end
end
body {
window {
title 'Todo MVC'
content_size 480, 480
margined true
vertical_box {
horizontal_box {
stretchy false
entry {
text <=> [@todo_list.new_todo, :task]
}
button('Add') {
stretchy false
on_clicked do
@todo_list.add_todo
end
}
}
horizontal_box {
stretchy false
button('Toggle All') {
stretchy false
on_clicked do
@todo_list.toggle_completion_of_all_todos
end
}
}
table {
checkbox_column('Completed') {
editable true
}
text_column('Task')
cell_rows <=> [@todo_list, :displayed_todos]
selection <=> [@todo_list, :selection_index]
}
horizontal_box {
stretchy false
button('Delete') {
stretchy false
enabled <= [@todo_list, :selection_index, on_read: -> (value) { !!value }]
on_clicked do
@todo_list.delete_todo
end
}
}
horizontal_box {
stretchy false
label {
stretchy false
text <= [@todo_list, :active_todos,
on_read: -> (todos) { "#{todos.count} item#{'s' if todos.size != 1} left" }
]
}
label # filler
button('All') {
stretchy false
enabled <= [@todo_list, :filter, on_read: -> (value) { value != :all }]
on_clicked do
@todo_list.filter = :all
end
}
button('Active') {
stretchy false
enabled <= [@todo_list, :filter, on_read: -> (value) { value != :active }]
on_clicked do
@todo_list.filter = :active
end
}
button('Completed') {
stretchy false
enabled <= [@todo_list, :filter, on_read: -> (value) { value != :completed }]
on_clicked do
@todo_list.filter = :completed
end
}
label # filler
button('Clear Completed') {
stretchy false
enabled <= [@todo_list, :completed_todos, on_read: :any?]
on_clicked do
@todo_list.clear_completed
end
}
}
}
}
}
end
end
end
Replace the content of app/todo_mvc/model/todo_list.rb
with the following code:
require 'todo_mvc/model/todo'
class TodoMvc
module Model
class TodoList
attr_accessor :todos, :active_todos, :completed_todos, :displayed_todos, :selection_index, :filter
def initialize
@todos = []
@active_todos = []
@completed_todos = []
@displayed_todos = @todos
@filter = :all
end
def add_todo(task = nil)
task ||= new_todo.task
todo = Todo.new(task, todo_list: self)
todos << todo
recalculate_filtered_todos
new_todo.task = ''
end
def new_todo
@new_todo ||= Todo.new('')
end
def delete_todo
@todos.delete_at(selection_index)
recalculate_filtered_todos
end
def toggle_completion_of_all_todos
if @todos.any?(&:active)
@todos.select(&:active).each(&:mark_completed)
else
@todos.select(&:completed).each(&:mark_active)
end
end
def recalculate_filtered_todos
self.completed_todos = @todos.select(&:completed)
self.active_todos = @todos.select(&:active)
recalculate_displayed_todos
end
def filter=(filter_value)
@filter = filter_value
recalculate_displayed_todos
end
def recalculate_displayed_todos
case filter
when :all
self.displayed_todos = todos
when :active
self.displayed_todos = active_todos
when :completed
self.displayed_todos = completed_todos
end
end
def clear_completed
@completed_todos.each { |todo| @todos.delete(todo) }
recalculate_filtered_todos
end
end
end
end
Run application by running terminal command:
glimmer run
Replace the content of app/todo_mvc/view/todo_mvc.rb
with the following code:
require 'todo_mvc/model/todo_list'
require 'todo_mvc/view/add_todo_form'
require 'todo_mvc/view/toggle_all_bar'
require 'todo_mvc/view/todo_table'
require 'todo_mvc/view/delete_bar'
require 'todo_mvc/view/filter_bar'
class TodoMvc
module View
class TodoMvc
include Glimmer::LibUI::Application
before_body do
@todo_list = Model::TodoList.new
['Home Improvement', 'Shopping', 'Cleaning'].each do |task|
@todo_list.add_todo(task)
end
end
body {
window {
title 'Todo MVC'
content_size 480, 480
margined true
vertical_box {
add_todo_form(todo_list: @todo_list) {
stretchy false
}
toggle_all_bar(todo_list: @todo_list) {
stretchy false
}
todo_table(todo_list: @todo_list)
delete_bar(todo_list: @todo_list) {
stretchy false
}
filter_bar(todo_list: @todo_list) {
stretchy false
}
}
}
}
end
end
end
Create app/todo_mvc/view/add_todo_form.rb
component by running:
glimmer "scaffold:customcontrol[add_todo_form]"
Replace the content of app/todo_mvc/view/add_todo_form.rb
with the following code:
class TodoMvc
module View
class AddTodoForm
include Glimmer::LibUI::CustomControl
option :todo_list
body {
horizontal_box {
entry {
text <=> [todo_list.new_todo, :task]
}
button('Add') {
stretchy false
on_clicked do
todo_list.add_todo
end
}
}
}
end
end
end
Create app/todo_mvc/view/toggle_all_bar.rb
component by running:
glimmer "scaffold:customcontrol[toggle_all_bar]"
Replace the content of app/todo_mvc/view/toggle_all_bar.rb
with the following code:
class TodoMvc
module View
class ToggleAllBar
include Glimmer::LibUI::CustomControl
option :todo_list
body {
horizontal_box {
button('Toggle All') {
stretchy false
on_clicked do
todo_list.toggle_completion_of_all_todos
end
}
}
}
end
end
end
Create app/todo_mvc/view/todo_table.rb
component by running:
glimmer "scaffold:customcontrol[todo_table]"
Replace the content of app/todo_mvc/view/todo_table.rb
with the following code:
class TodoMvc
module View
class TodoTable
include Glimmer::LibUI::CustomControl
option :todo_list
body {
table {
checkbox_column('Completed') {
editable true
}
text_column('Task')
cell_rows <=> [todo_list, :displayed_todos]
selection <=> [todo_list, :selection_index]
}
}
end
end
end
Create app/todo_mvc/view/delete_bar.rb
component by running:
glimmer "scaffold:customcontrol[delete_bar]"
Replace the content of app/todo_mvc/view/delete_bar.rb
with the following code:
class TodoMvc
module View
class DeleteBar
include Glimmer::LibUI::CustomControl
option :todo_list
body {
horizontal_box {
button('Delete') {
stretchy false
enabled <= [todo_list, :selection_index, on_read: -> (value) { !!value }]
on_clicked do
todo_list.delete_todo
end
}
}
}
end
end
end
Create app/todo_mvc/view/filter_bar.rb
component by running:
glimmer "scaffold:customcontrol[filter_bar]"
Replace the content of app/todo_mvc/view/filter_bar.rb
with the following code:
class TodoMvc
module View
class FilterBar
include Glimmer::LibUI::CustomControl
option :todo_list
body {
horizontal_box {
stretchy false
label {
stretchy false
text <= [todo_list, :active_todos,
on_read: -> (todos) { "#{todos.count} item#{'s' if todos.size != 1} left" }
]
}
label # filler
button('All') {
stretchy false
enabled <= [todo_list, :filter, on_read: -> (value) { value != :all }]
on_clicked do
todo_list.filter = :all
end
}
button('Active') {
stretchy false
enabled <= [todo_list, :filter, on_read: -> (value) { value != :active }]
on_clicked do
todo_list.filter = :active
end
}
button('Completed') {
stretchy false
enabled <= [todo_list, :filter, on_read: -> (value) { value != :completed }]
on_clicked do
todo_list.filter = :completed
end
}
label # filler
button('Clear Completed') {
stretchy false
enabled <= [todo_list, :completed_todos, on_read: :any?]
on_clicked do
todo_list.clear_completed
end
}
}
}
end
end
end
Run application by running terminal command:
glimmer run
Copyright (c) 2024 Andy Maleh. See LICENSE.txt for further details.