The Lua Carousel "productivity suite"


Another new version of Lua Carousel today. Not because of any new bugs or features, but a few small changes to Carousel's primitives as I try to use them in scripts.

In particular, I've included the library for writing .wav files in the last post into Lua Carousel. Now the voice recorder should be much easier to get running.

But I'm getting ahead of myself. I want to show 4 different little programs today that illustrate primitives I often reach for in my programs: buttons, sliders, editors and audio recorders/players.

The first example illustrates how to create a button that does something when you press it. Here the yellow rectangle is a button:

This script requires 30 lines of code:

ui_state = {}
shape = 'circle'
function car.draw()
  ui_state.button_handlers = {}                -- A
  -- draw a red shape
  color(1,0,0)
  if shape == 'circle' then
    circle('fill', 100, 100, 40)
  elseif shape == 'square' then
    rect('fill', 60,60, 80,80)
  end
  -- draw a yellow button that toggles the shape
  button(ui_state, 'toggle', {
    x=100-20,y=100+50+20,w=40,h=20,
    bg={r=1, g=1, b=0},
    icon=function(p)
      color(0,0,0)
      rect('line', p.x,p.y, p.w,p.h)
    end,
    onpress1=function()
      if shape == 'circle' then shape = 'square'
      else shape = 'circle' end
    end,
  })
end
function car.mousepressed(x,y, b)
  if mouse_press_consumed_by_any_button(ui_state, x,y, b) then
    return
  end
end

Add a line of code (A) to `car.draw` and a second block to `car.mousepressed`, and now you can add as many buttons as you want by calling `button` in `car.draw`.

Example 2 illustrates how to create a slider that does something as you slide it left and right:

This requires another 30 lines of code:

ui_state = {}
radius=40
function car.draw()
  ui_state.slider_handlers = {}                      -- A
  -- draw a red circle
  color(1,0,0)
  circle('fill', 100, 100, radius)
  -- draw a blue slider to adjust its size
  slider(ui_state, 'radius', {
    fg={r=0, g=0, b=1},
    x0=60, x1=140,  -- left and right limit
    y0=200,  -- sliders are always horizontal
    w=20, h=20,  -- size of the knob on the slider
    -- what the slider does
    lo=10, hi=100,  -- min/max permitted radius
    value=radius,  -- where the knob is placed
    update=function(v) radius=v end,  -- what moving the knob does
  })
end
function car.mousepressed(x,y, b)
  if mouse_press_consumed_by_any_slider(ui_state, x,y, b) then
    return
  end end
function car.update()
  update_sliders(ui_state, love.mouse.getX())
end
function car.mousereleased(x,y, b)
  ui_state.selected_slider = nil
end

As before, add a line of code (A) to `car.draw` and some blocks to `car.mousepressed`, `car.mousereleased` and `car.update` (the last giving the slider its effect). Now you can add as many sliders as you want by calling `slider` in `car.draw`.

Example 3 is a text editor:

This takes 40 lines of code:

e = edit.initialize_state(
  Menu_bottom+10, 100,  -- top and bottom
  50, 150,  -- left and right
  g.getFont(), Font_height, Line_height)  -- use default font
e.filename = 'example.txt'
load_from_disk(e)
Text.redraw_all(e)
function car.draw()
  color(0.6,0.6,0.6)
  -- border with 5px padding
  rect('line', e.left-5, e.top-5, e.width+5*2, (e.bottom+5)-(e.top-5))
  edit.draw(e, {r=0, g=0.6, b=0.6})  -- text color
end
function car.keychord_press(chord, key)
  edit.keychord_press(e, chord, key)
end
function car.text_input(t)
  edit.text_input(e, t)
end
function car.key_release(key)
  edit.key_release(e, key)
end
function car.mouse_press(x,y, b)
  edit.mouse_press(e, x,y, b)
end
function car.mouse_release(x,y, b)
  edit.mouse_release(e, x,y, b)
end
function car.update(dt)
  edit.update(e, dt)  -- mainly to periodically autosave
end
function car.quit()
  edit.quit(e)  -- make sure to save
end

Again, the code consists of a few lines in various handlers. This is the exact editor used to build Carousel, and it's also heavily used in all my other forks. A hundred automated tests ensure it's working every time Carousel starts up. It supports file loading and saving, copy/paste, find, undo/redo, though some features currently don't work on mobile devices. For example, there's no way to scroll on my phone since I don't have arrow keys. I still need to package up Carousel's scrollbars so scripts can easily use them.

(One thing you might have noticed is that the handlers have slightly different names. The editor supports keychords like `C-a` for ctrl-a, `M-f` for alt-f, `M-S-left` for alt+shift+left arrow, etc. There's a new handler to easily respond to keychords even outside the editor. But `keychordpressed` felt like a mouthful so I ended up creating a whole parallel set of names: mouse_press, mouse_release, mouse_wheel_move, keychord_press, key_release, text_input. You can use either these names or the standard ones if you're used to them.)

(Correction: on 2024-02-16, Carousel version bf added an argument to `edit.initialize_state` for the font to use with an editor. To keep the above example working, I added the argument `g.getFont()`. I hope not to introduce further incompatibilities.)

Moving on, the final example involves recording and playing audio. This is a much simpler variant of the previous post. Unfortunately this still won't work on iOS, and on Android you'll need to grant LÖVE access to your microphone. Press the orange button to record, the red button to stop recording, and the green button to play what you recorded:

This script requires 50 lines of code, mostly to describe the 3 buttons:

ui_state = {}
-- initialize recorder and player
local devices = love.audio.getRecordingDevices()
if #devices == 0 then
  error('no recording devices')
end
recording_device = devices[1]
recording = nil
playing_source = nil
function car.draw()
  ui_state.button_handlers = {}
  -- orange button to start recording
  button(ui_state, 'record', {
    x=50, y=50, w=60, h=40,
    bg={r=1, g=0.6, b=0},
    onpress1 = record,
  })
  -- red button to stop recording
  button(ui_state, 'stop', {
    x=120, y=50, w=60, h=40,
    bg={r=1, g=0, b=0},
    onpress1 = stop_recording,
  })
  -- green button to play what we recorded
  button(ui_state, 'play', {
    x=190, y=50, w=60, h=40,
    bg={r=0, g=1, b=0},
    onpress1 = play,
  })
end
function record()
  recording_device:start(10*8000, 8000)
  playing_source = nil
end
function stop_recording(recording_idx)
  recording = recording_device:getData()
  recording_device:stop()
end
function play(recording_idx)
  playing_source = audio(recording, 'static')
  playing_source:play()
end
function car.mousepressed(x,y, b)
  if mouse_press_consumed_by_any_button(ui_state, x,y, b) then
    return
  end
end

The core is the dozen or so lines near the bottom, using a `RecordingDevice` to control the microphone, `audio` to control the speaker and `save_wav`, now included in Lua Carousel, which takes a filename and a `SoundData` to save in it.

I'll stop here. Hopefully this has sparked some ideas for combining these primitives to help you in your life. As always, I'm happy to answer questions if something is unclear.

If you try pasting any of these programs into Lua Carousel, remember to first run the abbreviations on one of the example screens. Or if you've deleted that screen, here are the abbreviations I used in this post:

g = love.graphics
circle, rect = g.circle, g.rectangle
color = g.setColor
audio = love.audio.newSource

Correction: An earlier version of this post didn't specify the second argument to `audio` (love.audio.newSource). It's not required in practice, but the documentation doesn't mention a default value, and I've since seen strange distracting errors if I run this program without granting LÖVE access to the microphone. Just always include the second arg.

Files

carousel-bd.love 111 kB
Dec 20, 2023
carousel-bd-safe.love 111 kB
Dec 20, 2023

Get Lua Carousel

Leave a comment

Log in with itch.io to leave a comment.