package objects import "math" import "image" import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo/input" import "git.tebibyte.media/tomo/tomo/event" var _ tomo.Object = new(Slider) // Slider is a control that selects a numeric value between 0 and 1. // // Sub-components: // - SliderHandle is the grabbable handle of the slider. // // Tags: // - [vertical] The slider is oriented vertically. // - [horizontall] The slider is oriented horizontally. // // SliderHandle tags: // - [vertical] The handle is oriented vertically. // - [horizontall] The handle is oriented horizontally. type Slider struct { box tomo.ContainerBox handle *sliderHandle layout sliderLayout dragging bool dragOffset image.Point step float64 on struct { valueChange event.FuncBroadcaster confirm event.FuncBroadcaster } } type sliderHandle struct { tomo.Box } func newSlider (orient string, value float64) *Slider { slider := &Slider { box: tomo.NewContainerBox(), handle: &sliderHandle { Box: tomo.NewBox(), }, layout: sliderLayout { vertical: orient == "vertical", value: math.NaN(), }, step: 0.05, } slider.handle.SetRole(tomo.R("objects", "SliderHandle")) slider.handle.SetTag(orient, true) slider.box.SetRole(tomo.R("objects", "Slider")) slider.box.SetTag(orient, true) slider.box.Add(slider.handle) slider.box.SetFocusable(true) slider.SetValue(value) slider.box.SetInputMask(true) slider.box.OnKeyUp(slider.handleKeyUp) slider.box.OnKeyDown(slider.handleKeyDown) slider.box.OnButtonDown(slider.handleButtonDown) slider.box.OnButtonUp(slider.handleButtonUp) slider.box.OnMouseMove(slider.handleMouseMove) slider.box.OnScroll(slider.handleScroll) return slider } // NewVerticalSlider creates a new vertical slider with the specified value. func NewVerticalSlider (value float64) *Slider { return newSlider("vertical", value) } // NewHorizontalSlider creates a new horizontal slider with the specified value. func NewHorizontalSlider (value float64) *Slider { return newSlider("horizontal", value) } // GetBox returns the underlying box. func (this *Slider) GetBox () tomo.Box { return this.box } // SetFocused sets whether or not this slider has keyboard focus. If set to // true, this method will steal focus away from whichever object currently has // focus. func (this *Slider) SetFocused (focused bool) { this.box.SetFocused(focused) } // SetValue sets the value of the slider between 0 and 1. func (this *Slider) SetValue (value float64) { if value < 0 { value = 0 } if value > 1 { value = 1 } if value == this.layout.value { return } this.layout.value = value this.box.SetAttr(tomo.ALayout(this.layout)) } // Value returns the value of the slider between 0 and 1. func (this *Slider) Value () float64 { return this.layout.value } // OnValueChange specifies a function to be called when the user moves the // slider. func (this *Slider) OnValueChange (callback func ()) event.Cookie { return this.on.valueChange.Connect(callback) } // OnConfirm specifies a function to be called when the user stops moving the // slider. func (this *Slider) OnConfirm (callback func ()) event.Cookie { return this.on.confirm.Connect(callback) } func (this *Slider) handleKeyDown (key input.Key, numpad bool) bool { var increment float64; if this.layout.vertical { increment = -0.05 } else { increment = 0.05 } switch key { case input.KeyUp, input.KeyLeft: if this.box.Window().Modifiers().Alt { this.SetValue(0) } else { this.SetValue(this.Value() - increment) } this.on.valueChange.Broadcast() return true case input.KeyDown, input.KeyRight: if this.box.Window().Modifiers().Alt { this.SetValue(1) } else { this.SetValue(this.Value() + increment) } this.on.valueChange.Broadcast() return true case input.KeyHome: this.SetValue(0) this.on.valueChange.Broadcast() return true case input.KeyEnd: this.SetValue(1) this.on.valueChange.Broadcast() return true } return false } func (this *Slider) handleKeyUp (key input.Key, numpad bool) bool { switch key { case input.KeyUp, input.KeyLeft: return true case input.KeyDown, input.KeyRight: return true case input.KeyHome: return true case input.KeyEnd: return true } return false } func (this *Slider) handleButtonDown (button input.Button) bool { pointer := this.box.Window().MousePosition() handle := this.handle.Bounds() within := pointer.In(handle) var above bool; if this.layout.vertical { above = pointer.Y < handle.Min.Y + handle.Dy() / 2 } else { above = pointer.X < handle.Min.X + handle.Dx() / 2 } switch button { case input.ButtonLeft: if within { this.dragging = true this.dragOffset = pointer.Sub(this.handle.Bounds().Min). Add(this.box.InnerBounds().Min) this.drag() } else { this.dragOffset = this.fallbackDragOffset() this.dragging = true this.drag() } case input.ButtonMiddle: if above { this.SetValue(0) this.on.valueChange.Broadcast() this.on.confirm.Broadcast() } else { this.SetValue(1) this.on.valueChange.Broadcast() this.on.confirm.Broadcast() } case input.ButtonRight: if above { this.SetValue(this.Value() - this.step) this.on.valueChange.Broadcast() this.on.confirm.Broadcast() } else { this.SetValue(this.Value() + this.step) this.on.valueChange.Broadcast() this.on.confirm.Broadcast() } } return true } func (this *Slider) handleButtonUp (button input.Button) bool { if button != input.ButtonLeft || !this.dragging { return true } this.dragging = false this.on.confirm.Broadcast() return true } func (this *Slider) handleMouseMove () bool { if !this.dragging { return false } this.drag() return true } func (this *Slider) handleScroll (x, y float64) bool { delta := (x + y) * 0.005 this.SetValue(this.Value() + delta) this.on.valueChange.Broadcast() this.on.confirm.Broadcast() return true } func (this *Slider) drag () { pointer := this.box.Window().MousePosition().Sub(this.dragOffset) gutter := this.box.InnerBounds() handle := this.handle.Bounds() if this.layout.vertical { this.SetValue ( 1 - float64(pointer.Y) / float64(gutter.Dy() - handle.Dy())) } else { this.SetValue ( float64(pointer.X) / float64(gutter.Dx() - handle.Dx())) } this.on.valueChange.Broadcast() } func (this *Slider) fallbackDragOffset () image.Point { if this.layout.vertical { return this.box.InnerBounds().Min. Add(image.Pt(0, this.handle.Bounds().Dy() / 2)) } else { return this.box.InnerBounds().Min. Add(image.Pt(this.handle.Bounds().Dx() / 2, 0)) } } type sliderLayout struct { vertical bool value float64 } func (sliderLayout) MinimumSize (hints tomo.LayoutHints, boxes tomo.BoxQuerier) image.Point { if boxes.Len() != 1 { return image.Pt(0, 0) } return boxes.MinimumSize(0) } func (this sliderLayout) Arrange (hints tomo.LayoutHints, boxes tomo.BoxArranger) { if boxes.Len() != 1 { return } handle := image.Rectangle { Max: boxes.MinimumSize(0) } gutter := hints.Bounds if this.vertical { height := gutter.Dy() - handle.Dy() offset := int(float64(height) * (1 - this.value)) handle.Max.X = gutter.Dx() boxes.SetBounds ( 0, handle.Add(image.Pt(0, offset)).Add(gutter.Min)) } else { width := gutter.Dx() - handle.Dx() offset := int(float64(width) * this.value) handle.Max.Y = gutter.Dy() boxes.SetBounds ( 0, handle.Add(image.Pt(offset, 0)).Add(gutter.Min)) } } func (this sliderLayout) RecommendedHeight (hints tomo.LayoutHints, boxes tomo.BoxQuerier, width int) int { return this.MinimumSize(hints, boxes).X } func (this sliderLayout) RecommendedWidth (hints tomo.LayoutHints, boxes tomo.BoxQuerier, height int) int { return this.MinimumSize(hints, boxes).Y }