2023-04-18 16:37:50 -06:00
|
|
|
package elements
|
2023-03-21 10:26:48 -06:00
|
|
|
|
2023-03-23 12:11:42 -06:00
|
|
|
import "time"
|
2023-03-21 10:26:48 -06:00
|
|
|
import "io/fs"
|
2023-03-21 16:03:31 -06:00
|
|
|
import "image"
|
2023-03-30 23:06:29 -06:00
|
|
|
import "git.tebibyte.media/sashakoshka/tomo"
|
2023-03-23 12:11:42 -06:00
|
|
|
import "git.tebibyte.media/sashakoshka/tomo/input"
|
2023-03-21 16:03:31 -06:00
|
|
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
2023-04-18 16:37:50 -06:00
|
|
|
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
2023-03-30 23:06:29 -06:00
|
|
|
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
|
|
|
|
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
2023-03-21 10:26:48 -06:00
|
|
|
|
2023-04-18 16:37:50 -06:00
|
|
|
type fileEntity interface {
|
|
|
|
tomo.SelectableEntity
|
|
|
|
tomo.FocusableEntity
|
|
|
|
}
|
|
|
|
|
2023-03-21 16:03:31 -06:00
|
|
|
// File displays an interactive visual representation of a file within any
|
|
|
|
// file system.
|
2023-03-21 10:26:48 -06:00
|
|
|
type File struct {
|
2023-04-18 16:37:50 -06:00
|
|
|
entity fileEntity
|
2023-03-21 16:03:31 -06:00
|
|
|
|
|
|
|
config config.Wrapped
|
|
|
|
theme theme.Wrapped
|
2023-03-23 12:11:42 -06:00
|
|
|
|
|
|
|
lastClick time.Time
|
|
|
|
pressed bool
|
2023-04-18 16:37:50 -06:00
|
|
|
enabled bool
|
2023-03-30 23:06:29 -06:00
|
|
|
iconID tomo.Icon
|
2023-03-21 10:26:48 -06:00
|
|
|
filesystem fs.StatFS
|
2023-03-21 16:03:31 -06:00
|
|
|
location string
|
2023-03-23 12:11:42 -06:00
|
|
|
|
2023-03-21 16:03:31 -06:00
|
|
|
onChoose func ()
|
2023-03-21 10:26:48 -06:00
|
|
|
}
|
|
|
|
|
2023-03-21 16:03:31 -06:00
|
|
|
// NewFile creates a new file element. If within is nil, it will use the OS file
|
|
|
|
// system
|
2023-03-21 10:26:48 -06:00
|
|
|
func NewFile (
|
|
|
|
location string,
|
|
|
|
within fs.StatFS,
|
|
|
|
) (
|
|
|
|
element *File,
|
|
|
|
err error,
|
|
|
|
) {
|
2023-04-18 16:37:50 -06:00
|
|
|
element = &File { enabled: true }
|
2023-03-30 23:06:29 -06:00
|
|
|
element.theme.Case = tomo.C("files", "file")
|
2023-04-18 16:37:50 -06:00
|
|
|
element.entity = tomo.NewEntity(element).(fileEntity)
|
2023-03-21 10:26:48 -06:00
|
|
|
err = element.SetLocation(location, within)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-04-18 16:37:50 -06:00
|
|
|
// Entity returns this element's entity.
|
|
|
|
func (element *File) Entity () tomo.Entity {
|
|
|
|
return element.entity
|
|
|
|
}
|
|
|
|
|
|
|
|
// Draw causes the element to draw to the specified destination canvas.
|
|
|
|
func (element *File) Draw (destination canvas.Canvas) {
|
|
|
|
// background
|
|
|
|
state := element.state()
|
|
|
|
bounds := element.entity.Bounds()
|
|
|
|
sink := element.theme.Sink(tomo.PatternButton)
|
|
|
|
element.theme.
|
|
|
|
Pattern(tomo.PatternButton, state).
|
|
|
|
Draw(destination, bounds)
|
|
|
|
|
|
|
|
// icon
|
|
|
|
icon := element.icon()
|
|
|
|
if icon != nil {
|
|
|
|
iconBounds := icon.Bounds()
|
|
|
|
offset := image.Pt (
|
|
|
|
(bounds.Dx() - iconBounds.Dx()) / 2,
|
|
|
|
(bounds.Dy() - iconBounds.Dy()) / 2)
|
|
|
|
if element.pressed {
|
|
|
|
offset = offset.Add(sink)
|
|
|
|
}
|
|
|
|
icon.Draw (
|
|
|
|
destination,
|
|
|
|
element.theme.Color(tomo.ColorForeground, state),
|
|
|
|
bounds.Min.Add(offset))
|
|
|
|
}
|
|
|
|
}
|
2023-03-21 16:03:31 -06:00
|
|
|
// Location returns the file's location and filesystem.
|
2023-03-21 10:26:48 -06:00
|
|
|
func (element *File) Location () (string, fs.StatFS) {
|
|
|
|
return element.location, element.filesystem
|
|
|
|
}
|
|
|
|
|
2023-03-21 16:03:31 -06:00
|
|
|
// SetLocation sets the file's location and filesystem. If within is nil, it
|
|
|
|
// will use the OS file system.
|
2023-03-21 10:26:48 -06:00
|
|
|
func (element *File) SetLocation (
|
|
|
|
location string,
|
|
|
|
within fs.StatFS,
|
|
|
|
) error {
|
|
|
|
if within == nil {
|
|
|
|
within = defaultFS { }
|
|
|
|
}
|
|
|
|
element.location = location
|
|
|
|
element.filesystem = within
|
|
|
|
return element.Update()
|
|
|
|
}
|
|
|
|
|
2023-03-21 16:03:31 -06:00
|
|
|
// Update refreshes the element to match the file it represents.
|
2023-03-21 10:26:48 -06:00
|
|
|
func (element *File) Update () error {
|
|
|
|
info, err := element.filesystem.Stat(element.location)
|
|
|
|
|
2023-03-23 18:57:51 -06:00
|
|
|
if err != nil {
|
2023-03-30 23:06:29 -06:00
|
|
|
element.iconID = tomo.IconError
|
2023-03-23 18:57:51 -06:00
|
|
|
} else if info.IsDir() {
|
2023-03-30 23:06:29 -06:00
|
|
|
element.iconID = tomo.IconDirectory
|
2023-03-21 10:26:48 -06:00
|
|
|
} else {
|
2023-03-23 18:57:51 -06:00
|
|
|
// TODO: choose icon based on file mime type
|
2023-03-30 23:06:29 -06:00
|
|
|
element.iconID = tomo.IconFile
|
2023-03-21 10:26:48 -06:00
|
|
|
}
|
2023-03-21 16:03:31 -06:00
|
|
|
|
|
|
|
element.updateMinimumSize()
|
2023-04-18 16:37:50 -06:00
|
|
|
element.entity.Invalidate()
|
2023-03-21 10:26:48 -06:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-03-23 12:11:42 -06:00
|
|
|
func (element *File) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
|
|
|
if !element.Enabled() { return }
|
|
|
|
if key == input.KeyEnter {
|
|
|
|
element.pressed = true
|
2023-04-18 16:37:50 -06:00
|
|
|
element.entity.Invalidate()
|
2023-03-23 12:11:42 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (element *File) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
|
|
|
|
if key == input.KeyEnter && element.pressed {
|
|
|
|
element.pressed = false
|
|
|
|
if !element.Enabled() { return }
|
2023-04-18 16:37:50 -06:00
|
|
|
element.entity.Invalidate()
|
2023-03-23 12:11:42 -06:00
|
|
|
if element.onChoose != nil {
|
|
|
|
element.onChoose()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-18 16:37:50 -06:00
|
|
|
func (element *File) HandleFocusChange () {
|
|
|
|
element.entity.Invalidate()
|
|
|
|
}
|
|
|
|
|
2023-04-19 22:15:37 -06:00
|
|
|
func (element *File) HandleSelectionChange () {
|
|
|
|
element.entity.Invalidate()
|
|
|
|
}
|
|
|
|
|
2023-03-21 10:26:48 -06:00
|
|
|
func (element *File) OnChoose (callback func ()) {
|
|
|
|
element.onChoose = callback
|
|
|
|
}
|
2023-03-21 16:03:31 -06:00
|
|
|
|
2023-04-18 16:37:50 -06:00
|
|
|
// Focus gives this element input focus.
|
|
|
|
func (element *File) Focus () {
|
|
|
|
if !element.entity.Focused() { element.entity.Focus() }
|
|
|
|
}
|
|
|
|
|
|
|
|
// Enabled returns whether this file is enabled or not.
|
|
|
|
func (element *File) Enabled () bool {
|
|
|
|
return element.enabled
|
|
|
|
}
|
|
|
|
|
|
|
|
// SetEnabled sets whether this file is enabled or not.
|
|
|
|
func (element *File) SetEnabled (enabled bool) {
|
|
|
|
if element.enabled == enabled { return }
|
|
|
|
element.enabled = enabled
|
|
|
|
element.entity.Invalidate()
|
|
|
|
}
|
|
|
|
|
2023-03-23 12:11:42 -06:00
|
|
|
func (element *File) HandleMouseDown (x, y int, button input.Button) {
|
|
|
|
if !element.Enabled() { return }
|
2023-04-18 16:37:50 -06:00
|
|
|
if !element.entity.Focused() { element.Focus() }
|
2023-03-23 12:11:42 -06:00
|
|
|
if button != input.ButtonLeft { return }
|
|
|
|
element.pressed = true
|
2023-04-18 16:37:50 -06:00
|
|
|
element.entity.Invalidate()
|
2023-03-23 12:11:42 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
func (element *File) HandleMouseUp (x, y int, button input.Button) {
|
|
|
|
if button != input.ButtonLeft { return }
|
|
|
|
element.pressed = false
|
2023-04-18 16:37:50 -06:00
|
|
|
within := image.Point { x, y }.In(element.entity.Bounds())
|
2023-03-31 12:02:56 -06:00
|
|
|
if time.Since(element.lastClick) < element.config.DoubleClickDelay() {
|
2023-03-23 12:11:42 -06:00
|
|
|
if element.Enabled() && within && element.onChoose != nil {
|
|
|
|
element.onChoose()
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
element.lastClick = time.Now()
|
|
|
|
}
|
2023-04-18 16:37:50 -06:00
|
|
|
element.entity.Invalidate()
|
2023-03-23 12:11:42 -06:00
|
|
|
}
|
|
|
|
|
2023-03-23 13:55:18 -06:00
|
|
|
// SetTheme sets the element's theme.
|
2023-04-18 16:37:50 -06:00
|
|
|
func (element *File) SetTheme (theme tomo.Theme) {
|
|
|
|
if theme == element.theme.Theme { return }
|
|
|
|
element.theme.Theme = theme
|
|
|
|
element.entity.Invalidate()
|
2023-03-23 13:55:18 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
// SetConfig sets the element's configuration.
|
2023-04-18 16:37:50 -06:00
|
|
|
func (element *File) SetConfig (config tomo.Config) {
|
|
|
|
if config == element.config.Config { return }
|
|
|
|
element.config.Config = config
|
|
|
|
element.entity.Invalidate()
|
2023-03-23 13:55:18 -06:00
|
|
|
}
|
|
|
|
|
2023-03-30 23:06:29 -06:00
|
|
|
func (element *File) state () tomo.State {
|
|
|
|
return tomo.State {
|
2023-03-21 16:03:31 -06:00
|
|
|
Disabled: !element.Enabled(),
|
2023-04-18 16:37:50 -06:00
|
|
|
Focused: element.entity.Focused(),
|
2023-03-23 12:11:42 -06:00
|
|
|
Pressed: element.pressed,
|
2023-04-18 16:37:50 -06:00
|
|
|
On: element.entity.Selected(),
|
2023-03-21 16:03:31 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (element *File) icon () artist.Icon {
|
2023-03-30 23:06:29 -06:00
|
|
|
return element.theme.Icon(element.iconID, tomo.IconSizeLarge)
|
2023-03-21 16:03:31 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
func (element *File) updateMinimumSize () {
|
2023-03-30 23:06:29 -06:00
|
|
|
padding := element.theme.Padding(tomo.PatternButton)
|
2023-03-21 16:03:31 -06:00
|
|
|
icon := element.icon()
|
|
|
|
if icon == nil {
|
2023-04-18 16:37:50 -06:00
|
|
|
element.entity.SetMinimumSize (
|
2023-03-21 16:03:31 -06:00
|
|
|
padding.Horizontal(),
|
|
|
|
padding.Vertical())
|
|
|
|
} else {
|
|
|
|
bounds := padding.Inverse().Apply(icon.Bounds())
|
2023-04-18 16:37:50 -06:00
|
|
|
element.entity.SetMinimumSize(bounds.Dx(), bounds.Dy())
|
2023-03-21 16:03:31 -06:00
|
|
|
}
|
|
|
|
}
|