2023-02-08 19:05:36 -07:00
|
|
|
package fun
|
|
|
|
|
|
|
|
import "image"
|
|
|
|
import "git.tebibyte.media/sashakoshka/tomo/input"
|
|
|
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
|
|
|
import "git.tebibyte.media/sashakoshka/tomo/config"
|
|
|
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
|
|
|
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
2023-02-08 21:41:31 -07:00
|
|
|
import "git.tebibyte.media/sashakoshka/tomo/elements/fun/music"
|
2023-02-08 19:05:36 -07:00
|
|
|
|
|
|
|
const pianoKeyWidth = 18
|
|
|
|
|
|
|
|
type pianoKey struct {
|
|
|
|
image.Rectangle
|
2023-02-08 21:41:31 -07:00
|
|
|
music.Note
|
2023-02-08 19:05:36 -07:00
|
|
|
}
|
|
|
|
|
2023-02-09 12:50:24 -07:00
|
|
|
// Piano is an element that can be used to input midi notes.
|
2023-02-08 19:05:36 -07:00
|
|
|
type Piano struct {
|
|
|
|
*core.Core
|
2023-02-09 14:15:02 -07:00
|
|
|
*core.FocusableCore
|
2023-02-08 19:05:36 -07:00
|
|
|
core core.CoreControl
|
2023-02-09 14:15:02 -07:00
|
|
|
focusableControl core.FocusableCoreControl
|
2023-02-08 21:41:31 -07:00
|
|
|
low, high music.Octave
|
2023-02-08 19:05:36 -07:00
|
|
|
|
|
|
|
config config.Wrapped
|
|
|
|
theme theme.Wrapped
|
|
|
|
|
|
|
|
flatKeys []pianoKey
|
|
|
|
sharpKeys []pianoKey
|
|
|
|
|
|
|
|
pressed *pianoKey
|
2023-02-09 14:15:02 -07:00
|
|
|
keynavPressed map[music.Note] bool
|
2023-02-08 19:05:36 -07:00
|
|
|
|
2023-02-08 21:41:31 -07:00
|
|
|
onPress func (music.Note)
|
|
|
|
onRelease func (music.Note)
|
2023-02-08 19:05:36 -07:00
|
|
|
}
|
|
|
|
|
2023-02-09 12:50:24 -07:00
|
|
|
// NewPiano returns a new piano element with a lowest and highest octave,
|
|
|
|
// inclusive. If low is greater than high, they will be swapped.
|
2023-02-08 21:41:31 -07:00
|
|
|
func NewPiano (low, high music.Octave) (element *Piano) {
|
2023-02-09 12:50:24 -07:00
|
|
|
if low > high {
|
|
|
|
temp := low
|
|
|
|
low = high
|
|
|
|
high = temp
|
|
|
|
}
|
|
|
|
|
2023-02-08 19:05:36 -07:00
|
|
|
element = &Piano {
|
|
|
|
low: low,
|
|
|
|
high: high,
|
2023-02-09 14:15:02 -07:00
|
|
|
keynavPressed: make(map[music.Note] bool),
|
2023-02-08 19:05:36 -07:00
|
|
|
}
|
2023-02-09 14:15:02 -07:00
|
|
|
|
2023-02-08 19:05:36 -07:00
|
|
|
element.theme.Case = theme.C("fun", "piano")
|
|
|
|
element.Core, element.core = core.NewCore (func () {
|
|
|
|
element.recalculate()
|
|
|
|
element.draw()
|
|
|
|
})
|
2023-02-09 14:15:02 -07:00
|
|
|
element.FocusableCore,
|
|
|
|
element.focusableControl = core.NewFocusableCore(element.redo)
|
2023-02-08 19:05:36 -07:00
|
|
|
element.updateMinimumSize()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// OnPress sets a function to be called when a key is pressed.
|
2023-02-08 21:41:31 -07:00
|
|
|
func (element *Piano) OnPress (callback func (note music.Note)) {
|
2023-02-08 19:05:36 -07:00
|
|
|
element.onPress = callback
|
|
|
|
}
|
|
|
|
|
|
|
|
// OnRelease sets a function to be called when a key is released.
|
2023-02-08 21:41:31 -07:00
|
|
|
func (element *Piano) OnRelease (callback func (note music.Note)) {
|
2023-02-08 19:05:36 -07:00
|
|
|
element.onRelease = callback
|
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Piano) HandleMouseDown (x, y int, button input.Button) {
|
2023-02-09 14:15:02 -07:00
|
|
|
element.Focus()
|
2023-02-08 19:05:36 -07:00
|
|
|
if button != input.ButtonLeft { return }
|
|
|
|
element.pressUnderMouseCursor(image.Pt(x, y))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Piano) HandleMouseUp (x, y int, button input.Button) {
|
|
|
|
if button != input.ButtonLeft { return }
|
2023-02-09 14:15:02 -07:00
|
|
|
if element.onRelease != nil && element.pressed != nil {
|
2023-02-08 19:05:36 -07:00
|
|
|
element.onRelease((*element.pressed).Note)
|
|
|
|
}
|
|
|
|
element.pressed = nil
|
|
|
|
element.redo()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Piano) HandleMouseMove (x, y int) {
|
|
|
|
if element.pressed == nil { return }
|
|
|
|
element.pressUnderMouseCursor(image.Pt(x, y))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Piano) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
|
|
|
|
|
|
|
|
func (element *Piano) pressUnderMouseCursor (point image.Point) {
|
|
|
|
// find out which note is being pressed
|
|
|
|
newKey := (*pianoKey)(nil)
|
|
|
|
for index, key := range element.flatKeys {
|
|
|
|
if point.In(key.Rectangle) {
|
|
|
|
newKey = &element.flatKeys[index]
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for index, key := range element.sharpKeys {
|
|
|
|
if point.In(key.Rectangle) {
|
|
|
|
newKey = &element.sharpKeys[index]
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if newKey == nil { return }
|
|
|
|
|
|
|
|
if newKey != element.pressed {
|
2023-02-08 21:41:31 -07:00
|
|
|
// release previous note
|
|
|
|
if element.pressed != nil && element.onRelease != nil {
|
|
|
|
element.onRelease((*element.pressed).Note)
|
|
|
|
}
|
|
|
|
|
2023-02-08 19:05:36 -07:00
|
|
|
// press new note
|
|
|
|
element.pressed = newKey
|
|
|
|
if element.onPress != nil {
|
|
|
|
element.onPress((*element.pressed).Note)
|
|
|
|
}
|
|
|
|
element.redo()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-09 14:15:02 -07:00
|
|
|
var noteForKey = map[input.Key] music.Note {
|
|
|
|
'a': 60,
|
|
|
|
'w': 61,
|
|
|
|
's': 62,
|
|
|
|
'e': 63,
|
|
|
|
'd': 64,
|
|
|
|
'f': 65,
|
|
|
|
't': 66,
|
|
|
|
'g': 67,
|
|
|
|
'y': 68,
|
|
|
|
'h': 69,
|
|
|
|
'u': 70,
|
|
|
|
'j': 71,
|
|
|
|
'k': 72,
|
|
|
|
'o': 73,
|
|
|
|
'l': 74,
|
|
|
|
'p': 75,
|
|
|
|
';': 76,
|
|
|
|
'\'': 77,
|
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Piano) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
|
|
|
if !element.Enabled() { return }
|
|
|
|
note, exists := noteForKey[key]
|
|
|
|
if !exists { return }
|
|
|
|
if !element.keynavPressed[note] {
|
|
|
|
element.keynavPressed[note] = true
|
|
|
|
if element.onPress != nil {
|
|
|
|
element.onPress(note)
|
|
|
|
}
|
|
|
|
element.redo()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Piano) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
|
|
|
|
note, exists := noteForKey[key]
|
|
|
|
if !exists { return }
|
|
|
|
_, pressed := element.keynavPressed[note]
|
|
|
|
if !pressed { return }
|
|
|
|
delete(element.keynavPressed, note)
|
|
|
|
if element.onRelease != nil {
|
|
|
|
element.onRelease(note)
|
|
|
|
}
|
|
|
|
element.redo()
|
|
|
|
}
|
|
|
|
|
2023-02-08 19:05:36 -07:00
|
|
|
// SetTheme sets the element's theme.
|
|
|
|
func (element *Piano) SetTheme (new theme.Theme) {
|
|
|
|
if new == element.theme.Theme { return }
|
|
|
|
element.theme.Theme = new
|
|
|
|
element.updateMinimumSize()
|
|
|
|
element.recalculate()
|
|
|
|
element.redo()
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetConfig sets the element's configuration.
|
|
|
|
func (element *Piano) SetConfig (new config.Config) {
|
|
|
|
if new == element.config.Config { return }
|
|
|
|
element.config.Config = new
|
|
|
|
element.updateMinimumSize()
|
|
|
|
element.recalculate()
|
|
|
|
element.redo()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Piano) updateMinimumSize () {
|
2023-02-09 09:38:01 -07:00
|
|
|
inset := element.theme.Inset(theme.PatternSunken)
|
2023-02-08 19:05:36 -07:00
|
|
|
element.core.SetMinimumSize (
|
2023-02-09 09:38:01 -07:00
|
|
|
pianoKeyWidth * 7 * element.countOctaves() + inset[1] + inset[3],
|
|
|
|
64 + inset[0] + inset[2])
|
2023-02-08 19:05:36 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Piano) countOctaves () int {
|
|
|
|
return int(element.high - element.low + 1)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Piano) countFlats () int {
|
|
|
|
return element.countOctaves() * 8
|
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Piano) countSharps () int {
|
|
|
|
return element.countOctaves() * 5
|
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Piano) redo () {
|
|
|
|
if element.core.HasImage() {
|
|
|
|
element.draw()
|
|
|
|
element.core.DamageAll()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Piano) recalculate () {
|
|
|
|
element.flatKeys = make([]pianoKey, element.countFlats())
|
|
|
|
element.sharpKeys = make([]pianoKey, element.countSharps())
|
|
|
|
|
2023-02-09 09:38:01 -07:00
|
|
|
inset := element.theme.Inset(theme.PatternSunken)
|
|
|
|
bounds := inset.Apply(element.Bounds())
|
|
|
|
|
2023-02-08 19:05:36 -07:00
|
|
|
dot := bounds.Min
|
|
|
|
note := element.low.Note(0)
|
|
|
|
limit := element.high.Note(12)
|
|
|
|
flatIndex := 0
|
|
|
|
sharpIndex := 0
|
|
|
|
for note < limit {
|
|
|
|
if note.IsSharp() {
|
|
|
|
element.sharpKeys[sharpIndex].Rectangle = image.Rect (
|
|
|
|
-(pianoKeyWidth * 3) / 7, 0,
|
|
|
|
(pianoKeyWidth * 3) / 7,
|
2023-02-08 21:41:31 -07:00
|
|
|
(bounds.Dy() * 5) / 8).Add(dot)
|
2023-02-08 19:05:36 -07:00
|
|
|
element.sharpKeys[sharpIndex].Note = note
|
|
|
|
sharpIndex ++
|
|
|
|
} else {
|
|
|
|
element.flatKeys[flatIndex].Rectangle = image.Rect (
|
|
|
|
0, 0, pianoKeyWidth, bounds.Dy()).Add(dot)
|
|
|
|
dot.X += pianoKeyWidth
|
|
|
|
element.flatKeys[flatIndex].Note = note
|
|
|
|
flatIndex ++
|
|
|
|
}
|
|
|
|
note ++
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (element *Piano) draw () {
|
2023-02-09 14:15:02 -07:00
|
|
|
state := theme.PatternState {
|
|
|
|
Focused: element.Focused(),
|
|
|
|
Disabled: !element.Enabled(),
|
|
|
|
}
|
2023-02-09 09:38:01 -07:00
|
|
|
pattern := element.theme.Pattern(theme.PatternSunken, state)
|
|
|
|
// inset := element.theme.Inset(theme.PatternSunken)
|
|
|
|
artist.FillRectangle(element, pattern, element.Bounds())
|
|
|
|
|
2023-02-08 19:05:36 -07:00
|
|
|
for _, key := range element.flatKeys {
|
2023-02-09 14:15:02 -07:00
|
|
|
_, keynavPressed := element.keynavPressed[key.Note]
|
2023-02-08 19:05:36 -07:00
|
|
|
element.drawFlat (
|
|
|
|
key.Rectangle,
|
|
|
|
element.pressed != nil &&
|
2023-02-09 14:15:02 -07:00
|
|
|
(*element.pressed).Note == key.Note || keynavPressed,
|
|
|
|
state)
|
2023-02-08 19:05:36 -07:00
|
|
|
}
|
|
|
|
for _, key := range element.sharpKeys {
|
2023-02-09 14:15:02 -07:00
|
|
|
_, keynavPressed := element.keynavPressed[key.Note]
|
2023-02-08 19:05:36 -07:00
|
|
|
element.drawSharp (
|
|
|
|
key.Rectangle,
|
|
|
|
element.pressed != nil &&
|
2023-02-09 14:15:02 -07:00
|
|
|
(*element.pressed).Note == key.Note || keynavPressed,
|
|
|
|
state)
|
2023-02-08 19:05:36 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-09 14:15:02 -07:00
|
|
|
func (element *Piano) drawFlat (
|
|
|
|
bounds image.Rectangle,
|
|
|
|
pressed bool,
|
|
|
|
state theme.PatternState,
|
|
|
|
) {
|
|
|
|
state.Pressed = pressed
|
2023-02-09 09:38:01 -07:00
|
|
|
pattern := element.theme.Theme.Pattern (
|
|
|
|
theme.PatternButton, theme.C("fun", "flatKey"), state)
|
2023-02-08 19:05:36 -07:00
|
|
|
artist.FillRectangle(element, pattern, bounds)
|
|
|
|
}
|
|
|
|
|
2023-02-09 14:15:02 -07:00
|
|
|
func (element *Piano) drawSharp (
|
|
|
|
bounds image.Rectangle,
|
|
|
|
pressed bool,
|
|
|
|
state theme.PatternState,
|
|
|
|
) {
|
|
|
|
state.Pressed = pressed
|
2023-02-09 09:38:01 -07:00
|
|
|
pattern := element.theme.Theme.Pattern (
|
|
|
|
theme.PatternButton, theme.C("fun", "sharpKey"), state)
|
2023-02-08 19:05:36 -07:00
|
|
|
artist.FillRectangle(element, pattern, bounds)
|
|
|
|
}
|