Add audio selection
This commit is contained in:
@@ -18,7 +18,7 @@
|
|||||||
let
|
let
|
||||||
pkgs = import nixpkgs {
|
pkgs = import nixpkgs {
|
||||||
inherit system;
|
inherit system;
|
||||||
config.allowUnfreePredicate = pkg: (nixpkgs.lib.getName pkg) == "rhemawave";
|
config.allowUnfreePredicate = pkg: (nixpkgs.lib.getName pkg) == "radiantwave";
|
||||||
};
|
};
|
||||||
|
|
||||||
buildInputs = with pkgs; [
|
buildInputs = with pkgs; [
|
||||||
@@ -27,8 +27,8 @@
|
|||||||
SDL2_image
|
SDL2_image
|
||||||
SDL2_mixer
|
SDL2_mixer
|
||||||
sqlite
|
sqlite
|
||||||
dbus
|
|
||||||
] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [
|
] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [
|
||||||
|
dbus
|
||||||
libGL
|
libGL
|
||||||
wayland
|
wayland
|
||||||
wayland-protocols
|
wayland-protocols
|
||||||
@@ -36,15 +36,7 @@
|
|||||||
libdecor
|
libdecor
|
||||||
libx11
|
libx11
|
||||||
libxext
|
libxext
|
||||||
] ++ pkgs.lib.optionals pkgs.stdenv.isDarwin (with pkgs.darwin.apple_sdk.frameworks; [
|
]; # Darwin: system SDK provides OpenGL, AppKit, etc. automatically
|
||||||
OpenGL
|
|
||||||
CoreAudio
|
|
||||||
CoreFoundation
|
|
||||||
CoreServices
|
|
||||||
ForceFeedback
|
|
||||||
IOKit
|
|
||||||
AppKit
|
|
||||||
]);
|
|
||||||
|
|
||||||
nativeBuildInputs = with pkgs; [
|
nativeBuildInputs = with pkgs; [
|
||||||
go
|
go
|
||||||
|
|||||||
+199
-88
@@ -15,11 +15,8 @@ import (
|
|||||||
"rhemawavetech.com/rhemawave/internal/shaderManager"
|
"rhemawavetech.com/rhemawave/internal/shaderManager"
|
||||||
)
|
)
|
||||||
|
|
||||||
// focusToggle is the sentinel focusState value meaning the audio on/off toggle
|
|
||||||
// row has keyboard focus. Audio-file indices (0..n-1) are used for everything below.
|
|
||||||
const focusToggle = -1
|
const focusToggle = -1
|
||||||
|
|
||||||
// AudioFileList is kept as the canonical seed data for the audio_files table.
|
|
||||||
var AudioFileList = []db.AudioFile{
|
var AudioFileList = []db.AudioFile{
|
||||||
{ID: 14, Name: "Original Audio", Path: "OriginalAudio.wav", Selected: false, Order: 1},
|
{ID: 14, Name: "Original Audio", Path: "OriginalAudio.wav", Selected: false, Order: 1},
|
||||||
{ID: 1, Name: "174 Hz", Path: "174Hz.wav", Selected: false, Order: 2},
|
{ID: 1, Name: "174 Hz", Path: "174Hz.wav", Selected: false, Order: 2},
|
||||||
@@ -40,28 +37,22 @@ var AudioFileList = []db.AudioFile{
|
|||||||
const triangleSize = float32(16)
|
const triangleSize = float32(16)
|
||||||
|
|
||||||
type AudioOptions struct {
|
type AudioOptions struct {
|
||||||
// Page Attributes
|
|
||||||
Base
|
Base
|
||||||
font *fontManager.FontEntry
|
font *fontManager.FontEntry
|
||||||
textShader *shaderManager.Shader
|
textShader *shaderManager.Shader
|
||||||
solidShader *shaderManager.Shader
|
solidShader *shaderManager.Shader
|
||||||
|
|
||||||
// Page Textures
|
|
||||||
title StringItem
|
title StringItem
|
||||||
prompt StringItem
|
prompt StringItem
|
||||||
|
|
||||||
// Layout
|
|
||||||
verticalMargin int32
|
verticalMargin int32
|
||||||
ItemScale float32
|
ItemScale float32
|
||||||
|
|
||||||
// Audio toggle
|
|
||||||
AudioStatusToggle StringItem
|
AudioStatusToggle StringItem
|
||||||
|
|
||||||
// Audio file list — index 0 = "Dynamic Audio", index 1+ = AudioFileList entries
|
|
||||||
audioFileTextures []StringItem
|
audioFileTextures []StringItem
|
||||||
selectedFileIndex int // which item is currently selected (highlights green)
|
selectedFileIndex int
|
||||||
|
|
||||||
// Volume row
|
|
||||||
VolumeLabel StringItem
|
VolumeLabel StringItem
|
||||||
volumePercent StringItem
|
volumePercent StringItem
|
||||||
volumeBarX int32
|
volumeBarX int32
|
||||||
@@ -69,25 +60,46 @@ type AudioOptions struct {
|
|||||||
volumeBarW int32
|
volumeBarW int32
|
||||||
volumeBarH int32
|
volumeBarH int32
|
||||||
|
|
||||||
// Triangle cursor
|
|
||||||
triVAO uint32
|
triVAO uint32
|
||||||
triVBO uint32
|
triVBO uint32
|
||||||
triX float32
|
triX float32
|
||||||
triCenterY float32
|
triCenterY float32
|
||||||
spinElapsed float32
|
spinElapsed float32
|
||||||
|
|
||||||
// Focus: focusToggle (-1) = toggle row, 0..n-1 = audio file list item
|
|
||||||
focusState int
|
focusState int
|
||||||
}
|
|
||||||
|
|
||||||
// ── Init ──────────────────────────────────────────────────────────────────────
|
// Section header
|
||||||
|
sectionHeader StringItem
|
||||||
|
|
||||||
|
// Scrolling
|
||||||
|
scrollOffset int
|
||||||
|
maxVisible int
|
||||||
|
listItemMargin int32
|
||||||
|
listAreaTop int32
|
||||||
|
listAreaBottom int32
|
||||||
|
listItemHeight int32
|
||||||
|
scrollTrackWidth int32
|
||||||
|
scrollTrackX int32
|
||||||
|
scrollTrackY int32
|
||||||
|
scrollTrackH int32
|
||||||
|
|
||||||
|
// Selection dot
|
||||||
|
dotSize float32
|
||||||
|
|
||||||
|
// List left-alignment X (same for all items)
|
||||||
|
listLeftX int32
|
||||||
|
}
|
||||||
|
|
||||||
func (p *AudioOptions) Init(ctx *PageCtx) error {
|
func (p *AudioOptions) Init(ctx *PageCtx) error {
|
||||||
var err error
|
var err error
|
||||||
p.verticalMargin = int32(20)
|
p.verticalMargin = int32(20)
|
||||||
|
p.listItemMargin = 6
|
||||||
p.focusState = focusToggle
|
p.focusState = focusToggle
|
||||||
|
p.scrollOffset = 0
|
||||||
|
p.dotSize = 8.0
|
||||||
|
p.scrollTrackWidth = 30
|
||||||
|
p.scrollTrackX = p.ScreenWidth - p.scrollTrackWidth
|
||||||
|
|
||||||
// Font
|
|
||||||
fm := fontManager.Get()
|
fm := fontManager.Get()
|
||||||
font, ok := fm.GetFont("SpaceMonoNerdFont-Regular")
|
font, ok := fm.GetFont("SpaceMonoNerdFont-Regular")
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -95,7 +107,6 @@ func (p *AudioOptions) Init(ctx *PageCtx) error {
|
|||||||
}
|
}
|
||||||
p.font = font
|
p.font = font
|
||||||
|
|
||||||
// Shaders
|
|
||||||
sm := shaderManager.Get()
|
sm := shaderManager.Get()
|
||||||
p.textShader, ok = sm.Get("text")
|
p.textShader, ok = sm.Get("text")
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -119,14 +130,12 @@ func (p *AudioOptions) Init(ctx *PageCtx) error {
|
|||||||
titleScale := float32(64) / float32(baseFontSize)
|
titleScale := float32(64) / float32(baseFontSize)
|
||||||
p.ItemScale = float32(32) / float32(baseFontSize)
|
p.ItemScale = float32(32) / float32(baseFontSize)
|
||||||
|
|
||||||
// Title
|
|
||||||
p.title, err = NewStringItem("Audio Options", p.font, colors.White)
|
p.title, err = NewStringItem("Audio Options", p.font, colors.White)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("creating title: %w", err)
|
return fmt.Errorf("creating title: %w", err)
|
||||||
}
|
}
|
||||||
p.title.Scale(titleScale)
|
p.title.Scale(titleScale)
|
||||||
|
|
||||||
// Prompt
|
|
||||||
p.prompt, err = NewStringItem(
|
p.prompt, err = NewStringItem(
|
||||||
"Enter to select -- Up/Down to navigate -- Left/Right to change volume -- Backspace to go back",
|
"Enter to select -- Up/Down to navigate -- Left/Right to change volume -- Backspace to go back",
|
||||||
p.font, colors.White,
|
p.font, colors.White,
|
||||||
@@ -136,7 +145,6 @@ func (p *AudioOptions) Init(ctx *PageCtx) error {
|
|||||||
}
|
}
|
||||||
p.prompt.Scale(p.ItemScale)
|
p.prompt.Scale(p.ItemScale)
|
||||||
|
|
||||||
// Volume row
|
|
||||||
p.VolumeLabel, err = NewStringItem("Volume", p.font, colors.White)
|
p.VolumeLabel, err = NewStringItem("Volume", p.font, colors.White)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("creating volume label: %w", err)
|
return fmt.Errorf("creating volume label: %w", err)
|
||||||
@@ -146,20 +154,16 @@ func (p *AudioOptions) Init(ctx *PageCtx) error {
|
|||||||
logger.Error("failed to create VolumePercentItem in AudioOptions.Init()")
|
logger.Error("failed to create VolumePercentItem in AudioOptions.Init()")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Audio toggle
|
|
||||||
if err = p.CreateAudioStatusItem(); err != nil {
|
if err = p.CreateAudioStatusItem(); err != nil {
|
||||||
logger.Error("failed to create AudioStatusItem in AudioOptions.Init()")
|
logger.Error("failed to create AudioStatusItem in AudioOptions.Init()")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure the audio_files table is populated.
|
|
||||||
if err = p.seedAudioFiles(); err != nil {
|
if err = p.seedAudioFiles(); err != nil {
|
||||||
logger.Error("failed to seed audio files: %v", err)
|
logger.Error("failed to seed audio files: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the audio file list textures: "Dynamic Audio" + AudioFileList
|
|
||||||
p.buildAudioFileList()
|
p.buildAudioFileList()
|
||||||
|
|
||||||
// Triangle cursor VAO/VBO — stride is vec2 only, no tex coords
|
|
||||||
triVerts := []float32{
|
triVerts := []float32{
|
||||||
0.5, 0.0,
|
0.5, 0.0,
|
||||||
-0.5, 0.5,
|
-0.5, 0.5,
|
||||||
@@ -175,20 +179,22 @@ func (p *AudioOptions) Init(ctx *PageCtx) error {
|
|||||||
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
|
gl.BindBuffer(gl.ARRAY_BUFFER, 0)
|
||||||
gl.BindVertexArray(0)
|
gl.BindVertexArray(0)
|
||||||
|
|
||||||
|
p.sectionHeader, err = NewStringItem("Audio Track", p.font, colors.Silver)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("creating section header: %w", err)
|
||||||
|
}
|
||||||
|
p.sectionHeader.Scale(p.ItemScale * 1.2)
|
||||||
|
|
||||||
p.layout()
|
p.layout()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildAudioFileList creates textures for the audio file list.
|
|
||||||
// Index 0 = "Dynamic Audio", index 1+ = AudioFileList entries.
|
|
||||||
func (p *AudioOptions) buildAudioFileList() {
|
func (p *AudioOptions) buildAudioFileList() {
|
||||||
// Clear old textures
|
|
||||||
for i := range p.audioFileTextures {
|
for i := range p.audioFileTextures {
|
||||||
gl.DeleteTextures(1, &p.audioFileTextures[i].ID)
|
gl.DeleteTextures(1, &p.audioFileTextures[i].ID)
|
||||||
}
|
}
|
||||||
p.audioFileTextures = nil
|
p.audioFileTextures = nil
|
||||||
|
|
||||||
// "Dynamic Audio" entry at index 0
|
|
||||||
dynamicItem, err := NewStringItem("Dynamic Audio", p.font, colors.White)
|
dynamicItem, err := NewStringItem("Dynamic Audio", p.font, colors.White)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("failed to create Dynamic Audio string item: %v", err)
|
logger.Error("failed to create Dynamic Audio string item: %v", err)
|
||||||
@@ -197,7 +203,6 @@ func (p *AudioOptions) buildAudioFileList() {
|
|||||||
p.audioFileTextures = append(p.audioFileTextures, dynamicItem)
|
p.audioFileTextures = append(p.audioFileTextures, dynamicItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AudioFileList entries at index 1+
|
|
||||||
for _, af := range AudioFileList {
|
for _, af := range AudioFileList {
|
||||||
item, err := NewStringItem(af.Name, p.font, colors.White)
|
item, err := NewStringItem(af.Name, p.font, colors.White)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -208,15 +213,13 @@ func (p *AudioOptions) buildAudioFileList() {
|
|||||||
p.audioFileTextures = append(p.audioFileTextures, item)
|
p.audioFileTextures = append(p.audioFileTextures, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine which item is currently selected
|
|
||||||
p.updateSelectedIndex()
|
p.updateSelectedIndex()
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateSelectedIndex sets selectedFileIndex based on the DB state.
|
|
||||||
func (p *AudioOptions) updateSelectedIndex() {
|
func (p *AudioOptions) updateSelectedIndex() {
|
||||||
config, err := db.GetConfig(p.Context.DbPtr)
|
config, err := db.GetConfig(p.Context.DbPtr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.selectedFileIndex = 0 // default to Dynamic Audio
|
p.selectedFileIndex = 0
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,27 +228,22 @@ func (p *AudioOptions) updateSelectedIndex() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the audio file with Selected = true
|
|
||||||
var selected db.AudioFile
|
var selected db.AudioFile
|
||||||
if err := p.Context.DbPtr.Where("selected = ?", true).First(&selected).Error; err != nil {
|
if err := p.Context.DbPtr.Where("selected = ?", true).First(&selected).Error; err != nil {
|
||||||
// No selection found — default to Dynamic Audio
|
|
||||||
p.selectedFileIndex = 0
|
p.selectedFileIndex = 0
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map the selected AudioFile ID to its index in the list
|
|
||||||
for i, af := range AudioFileList {
|
for i, af := range AudioFileList {
|
||||||
if af.ID == selected.ID {
|
if af.ID == selected.ID {
|
||||||
p.selectedFileIndex = i + 1 // +1 because index 0 is "Dynamic Audio"
|
p.selectedFileIndex = i + 1
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback — file not in list
|
|
||||||
p.selectedFileIndex = 0
|
p.selectedFileIndex = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// seedAudioFiles ensures the audio_files table has its default rows.
|
|
||||||
func (p *AudioOptions) seedAudioFiles() error {
|
func (p *AudioOptions) seedAudioFiles() error {
|
||||||
var existing []db.AudioFile
|
var existing []db.AudioFile
|
||||||
if err := p.Context.DbPtr.Find(&existing).Error; err != nil {
|
if err := p.Context.DbPtr.Find(&existing).Error; err != nil {
|
||||||
@@ -257,8 +255,6 @@ func (p *AudioOptions) seedAudioFiles() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Audio status toggle ───────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
func (p *AudioOptions) CreateAudioStatusItem() error {
|
func (p *AudioOptions) CreateAudioStatusItem() error {
|
||||||
config, err := db.GetConfig(p.Context.DbPtr)
|
config, err := db.GetConfig(p.Context.DbPtr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -280,9 +276,6 @@ func (p *AudioOptions) CreateAudioStatusItem() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Audio file selection ─────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
// selectAudioFile handles the user pressing Enter on an audio file list item.
|
|
||||||
func (p *AudioOptions) selectAudioFile(index int) {
|
func (p *AudioOptions) selectAudioFile(index int) {
|
||||||
config, err := db.GetConfig(p.Context.DbPtr)
|
config, err := db.GetConfig(p.Context.DbPtr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -290,18 +283,13 @@ func (p *AudioOptions) selectAudioFile(index int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if index == 0 {
|
if index == 0 {
|
||||||
// "Dynamic Audio" selected
|
|
||||||
config.DynamicAudio = true
|
config.DynamicAudio = true
|
||||||
// Clear all file selections
|
|
||||||
p.Context.DbPtr.Model(&db.AudioFile{}).Where("1 = 1").Update("selected", false)
|
p.Context.DbPtr.Model(&db.AudioFile{}).Where("1 = 1").Update("selected", false)
|
||||||
} else {
|
} else {
|
||||||
// Specific audio file selected
|
|
||||||
config.DynamicAudio = false
|
config.DynamicAudio = false
|
||||||
fileIndex := index - 1 // offset for "Dynamic Audio" entry
|
fileIndex := index - 1
|
||||||
if fileIndex >= 0 && fileIndex < len(AudioFileList) {
|
if fileIndex >= 0 && fileIndex < len(AudioFileList) {
|
||||||
// Clear all selections first
|
|
||||||
p.Context.DbPtr.Model(&db.AudioFile{}).Where("1 = 1").Update("selected", false)
|
p.Context.DbPtr.Model(&db.AudioFile{}).Where("1 = 1").Update("selected", false)
|
||||||
// Select the chosen file
|
|
||||||
p.Context.DbPtr.Model(&db.AudioFile{}).Where("id = ?", AudioFileList[fileIndex].ID).Update("selected", true)
|
p.Context.DbPtr.Model(&db.AudioFile{}).Where("id = ?", AudioFileList[fileIndex].ID).Update("selected", true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -313,8 +301,6 @@ func (p *AudioOptions) selectAudioFile(index int) {
|
|||||||
p.selectedFileIndex = index
|
p.selectedFileIndex = index
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Volume ────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
func (p *AudioOptions) CreateVolumePercentItem() error {
|
func (p *AudioOptions) CreateVolumePercentItem() error {
|
||||||
if p.volumePercent.ID != 0 {
|
if p.volumePercent.ID != 0 {
|
||||||
gl.DeleteTextures(1, &p.volumePercent.ID)
|
gl.DeleteTextures(1, &p.volumePercent.ID)
|
||||||
@@ -358,8 +344,6 @@ func (p *AudioOptions) UpdateVolume(level int) {
|
|||||||
p.volumePercent.Y = p.volumeBarY + (p.volumeBarH-p.volumePercent.H)/2
|
p.volumePercent.Y = p.volumeBarY + (p.volumeBarH-p.volumePercent.H)/2
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Layout ────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
func (p *AudioOptions) layout() {
|
func (p *AudioOptions) layout() {
|
||||||
config, err := db.GetConfig(p.Context.DbPtr)
|
config, err := db.GetConfig(p.Context.DbPtr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -392,25 +376,72 @@ func (p *AudioOptions) layout() {
|
|||||||
p.volumePercent.X = barX + barW + gap
|
p.volumePercent.X = barX + barW + gap
|
||||||
p.volumePercent.Y = barY + (barH-p.volumePercent.H)/2
|
p.volumePercent.Y = barY + (barH-p.volumePercent.H)/2
|
||||||
|
|
||||||
// Audio toggle — centred below the volume row
|
// Audio toggle
|
||||||
p.AudioStatusToggle.X = p.ScreenCenterX - p.AudioStatusToggle.W/2
|
p.AudioStatusToggle.X = p.ScreenCenterX - p.AudioStatusToggle.W/2
|
||||||
p.AudioStatusToggle.Y = barY - p.verticalMargin*3 - p.AudioStatusToggle.H
|
p.AudioStatusToggle.Y = barY - p.verticalMargin*3 - p.AudioStatusToggle.H
|
||||||
|
|
||||||
// Audio file list — each item centred and stacked below the toggle
|
// Section header — sits above the toggle, marks the start of the audio track list
|
||||||
nextY := p.AudioStatusToggle.Y - p.verticalMargin
|
p.sectionHeader.X = p.ScreenCenterX - p.sectionHeader.W/2
|
||||||
for i := range p.audioFileTextures {
|
p.sectionHeader.Y = p.AudioStatusToggle.Y - p.verticalMargin - p.sectionHeader.H
|
||||||
item := &p.audioFileTextures[i]
|
|
||||||
item.X = p.ScreenCenterX - item.W/2
|
// Scrollable list area boundaries
|
||||||
item.Y = nextY - item.H
|
p.listItemHeight = p.audioFileTextures[0].H
|
||||||
nextY = item.Y - p.verticalMargin
|
listTop := p.sectionHeader.Y - p.listItemMargin
|
||||||
|
listBottomMargin := int32(12)
|
||||||
|
|
||||||
|
// Available height for the list
|
||||||
|
listH := listTop - listBottomMargin
|
||||||
|
if listH < p.listItemHeight {
|
||||||
|
listH = p.listItemHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reposition the triangle cursor for whatever row currently has focus
|
p.listAreaTop = listTop
|
||||||
|
p.listAreaBottom = listBottomMargin
|
||||||
|
|
||||||
|
// How many items fit
|
||||||
|
itemStep := p.listItemHeight + p.listItemMargin
|
||||||
|
n := len(p.audioFileTextures)
|
||||||
|
p.maxVisible = int(listH / itemStep)
|
||||||
|
if p.maxVisible < 1 {
|
||||||
|
p.maxVisible = 1
|
||||||
|
}
|
||||||
|
if p.maxVisible > n {
|
||||||
|
p.maxVisible = n
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute left alignment: find the widest item to center the block
|
||||||
|
var maxItemW int32
|
||||||
|
for i := range p.audioFileTextures {
|
||||||
|
if p.audioFileTextures[i].W > maxItemW {
|
||||||
|
maxItemW = p.audioFileTextures[i].W
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.listLeftX = p.ScreenCenterX - maxItemW/2
|
||||||
|
|
||||||
|
// Position each item — scrollOffset shifts the whole list up/down
|
||||||
|
firstVisibleY := p.listAreaTop - p.listItemHeight
|
||||||
|
for i := range p.audioFileTextures {
|
||||||
|
item := &p.audioFileTextures[i]
|
||||||
|
item.X = p.listLeftX
|
||||||
|
item.Y = firstVisibleY - int32(i-p.scrollOffset)*itemStep
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp scroll offset so we never show empty space
|
||||||
|
maxScroll := n - p.maxVisible
|
||||||
|
if maxScroll < 0 {
|
||||||
|
maxScroll = 0
|
||||||
|
}
|
||||||
|
if p.scrollOffset > maxScroll {
|
||||||
|
p.scrollOffset = maxScroll
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scrollbar track position
|
||||||
|
p.scrollTrackY = p.listAreaBottom
|
||||||
|
p.scrollTrackH = p.listAreaTop - p.listAreaBottom
|
||||||
|
|
||||||
p.updateCursorPosition()
|
p.updateCursorPosition()
|
||||||
}
|
}
|
||||||
|
|
||||||
// updateCursorPosition moves triX / triCenterY to sit just left of the
|
|
||||||
// focused row. Call this any time focusState changes or layout() reruns.
|
|
||||||
func (p *AudioOptions) updateCursorPosition() {
|
func (p *AudioOptions) updateCursorPosition() {
|
||||||
cursorGap := int32(12)
|
cursorGap := int32(12)
|
||||||
|
|
||||||
@@ -428,8 +459,6 @@ func (p *AudioOptions) updateCursorPosition() {
|
|||||||
p.triCenterY = float32(item.Y) + float32(item.H)/2
|
p.triCenterY = float32(item.Y) + float32(item.H)/2
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Toggle ────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
func (p *AudioOptions) ToggleAudio() {
|
func (p *AudioOptions) ToggleAudio() {
|
||||||
config, err := db.GetConfig(p.Context.DbPtr)
|
config, err := db.GetConfig(p.Context.DbPtr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -442,32 +471,38 @@ func (p *AudioOptions) ToggleAudio() {
|
|||||||
p.layout()
|
p.layout()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Events ────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
func (p *AudioOptions) HandleEvent(event *sdl.Event) error {
|
func (p *AudioOptions) HandleEvent(event *sdl.Event) error {
|
||||||
e, ok := (*event).(*sdl.KeyboardEvent)
|
e, ok := (*event).(*sdl.KeyboardEvent)
|
||||||
if !ok || e.Type != sdl.KEYDOWN {
|
if !ok || e.Type != sdl.KEYDOWN {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
n := len(p.audioFileTextures)
|
||||||
|
|
||||||
switch e.Keysym.Sym {
|
switch e.Keysym.Sym {
|
||||||
case sdl.K_RETURN:
|
case sdl.K_RETURN:
|
||||||
if p.focusState == focusToggle {
|
if p.focusState == focusToggle {
|
||||||
p.ToggleAudio()
|
p.ToggleAudio()
|
||||||
} else if p.focusState >= 0 && p.focusState < len(p.audioFileTextures) {
|
} else if p.focusState >= 0 && p.focusState < n {
|
||||||
p.selectAudioFile(p.focusState)
|
p.selectAudioFile(p.focusState)
|
||||||
p.updateCursorPosition()
|
p.updateCursorPosition()
|
||||||
}
|
}
|
||||||
|
|
||||||
case sdl.K_DOWN:
|
case sdl.K_DOWN:
|
||||||
if p.focusState == focusToggle {
|
if p.focusState == focusToggle {
|
||||||
if len(p.audioFileTextures) > 0 {
|
if n > 0 {
|
||||||
p.focusState = 0
|
p.focusState = 0
|
||||||
|
p.scrollOffset = 0
|
||||||
|
p.layout()
|
||||||
|
}
|
||||||
|
} else if p.focusState < n-1 {
|
||||||
|
p.focusState++
|
||||||
|
if p.focusState >= p.scrollOffset+p.maxVisible {
|
||||||
|
p.scrollOffset++
|
||||||
|
p.layout()
|
||||||
|
} else {
|
||||||
p.updateCursorPosition()
|
p.updateCursorPosition()
|
||||||
}
|
}
|
||||||
} else if p.focusState < len(p.audioFileTextures)-1 {
|
|
||||||
p.focusState++
|
|
||||||
p.updateCursorPosition()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case sdl.K_UP:
|
case sdl.K_UP:
|
||||||
@@ -476,7 +511,35 @@ func (p *AudioOptions) HandleEvent(event *sdl.Event) error {
|
|||||||
p.updateCursorPosition()
|
p.updateCursorPosition()
|
||||||
} else if p.focusState > 0 {
|
} else if p.focusState > 0 {
|
||||||
p.focusState--
|
p.focusState--
|
||||||
p.updateCursorPosition()
|
if p.focusState < p.scrollOffset {
|
||||||
|
p.scrollOffset--
|
||||||
|
p.layout()
|
||||||
|
} else {
|
||||||
|
p.updateCursorPosition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case sdl.K_PAGEUP:
|
||||||
|
if p.focusState > 0 {
|
||||||
|
p.focusState -= p.maxVisible - 1
|
||||||
|
if p.focusState < 0 {
|
||||||
|
p.focusState = 0
|
||||||
|
}
|
||||||
|
p.scrollOffset = p.focusState
|
||||||
|
p.layout()
|
||||||
|
}
|
||||||
|
|
||||||
|
case sdl.K_PAGEDOWN:
|
||||||
|
if p.focusState < n-1 {
|
||||||
|
p.focusState += p.maxVisible - 1
|
||||||
|
if p.focusState >= n {
|
||||||
|
p.focusState = n - 1
|
||||||
|
}
|
||||||
|
p.scrollOffset = p.focusState - p.maxVisible + 1
|
||||||
|
if p.scrollOffset < 0 {
|
||||||
|
p.scrollOffset = 0
|
||||||
|
}
|
||||||
|
p.layout()
|
||||||
}
|
}
|
||||||
|
|
||||||
case sdl.K_RIGHT:
|
case sdl.K_RIGHT:
|
||||||
@@ -492,8 +555,6 @@ func (p *AudioOptions) HandleEvent(event *sdl.Event) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Update ────────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
func (p *AudioOptions) Update(dt float32) error {
|
func (p *AudioOptions) Update(dt float32) error {
|
||||||
p.spinElapsed += dt
|
p.spinElapsed += dt
|
||||||
if p.spinElapsed >= 1.0 {
|
if p.spinElapsed >= 1.0 {
|
||||||
@@ -502,8 +563,6 @@ func (p *AudioOptions) Update(dt float32) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Render helpers ────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
func (p *AudioOptions) renderVolumeBar() error {
|
func (p *AudioOptions) renderVolumeBar() error {
|
||||||
config, err := db.GetConfig(p.Context.DbPtr)
|
config, err := db.GetConfig(p.Context.DbPtr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -543,7 +602,6 @@ func (p *AudioOptions) renderVolumeBar() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderCursor draws the spinning triangle cursor next to the focused row.
|
|
||||||
func (p *AudioOptions) renderCursor() error {
|
func (p *AudioOptions) renderCursor() error {
|
||||||
scaleY := float32(math.Cos(2 * math.Pi * float64(p.spinElapsed)))
|
scaleY := float32(math.Cos(2 * math.Pi * float64(p.spinElapsed)))
|
||||||
|
|
||||||
@@ -570,7 +628,33 @@ func (p *AudioOptions) renderCursor() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Render ────────────────────────────────────────────────────────────────────
|
func (p *AudioOptions) renderScrollbar() {
|
||||||
|
n := len(p.audioFileTextures)
|
||||||
|
if n <= p.maxVisible {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
trackColor := sdl.Color{R: 255, G: 255, B: 255, A: 30}
|
||||||
|
thumbColor := sdl.Color{R: 255, G: 255, B: 255, A: 100}
|
||||||
|
|
||||||
|
p.RenderSolidColorQuad(p.solidShader,
|
||||||
|
mgl32.Vec2{float32(p.scrollTrackX), float32(p.scrollTrackY)},
|
||||||
|
mgl32.Vec2{float32(p.scrollTrackWidth), float32(p.scrollTrackH)},
|
||||||
|
trackColor)
|
||||||
|
|
||||||
|
visibleRatio := float32(p.maxVisible) / float32(n)
|
||||||
|
scrollRatio := float32(p.scrollOffset) / float32(n)
|
||||||
|
thumbHeight := float32(p.scrollTrackH) * visibleRatio
|
||||||
|
if thumbHeight < 20 {
|
||||||
|
thumbHeight = 20
|
||||||
|
}
|
||||||
|
thumbY := float32(p.scrollTrackY) + float32(p.scrollTrackH)*scrollRatio
|
||||||
|
|
||||||
|
p.RenderSolidColorQuad(p.solidShader,
|
||||||
|
mgl32.Vec2{float32(p.scrollTrackX + 2), thumbY},
|
||||||
|
mgl32.Vec2{float32(p.scrollTrackWidth - 4), thumbHeight},
|
||||||
|
thumbColor)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *AudioOptions) Render() error {
|
func (p *AudioOptions) Render() error {
|
||||||
if err := p.RenderTexture(p.textShader, p.title.ID, p.title.Position(), p.title.RawDimensions(), p.title.Color); err != nil {
|
if err := p.RenderTexture(p.textShader, p.title.ID, p.title.Position(), p.title.RawDimensions(), p.title.Color); err != nil {
|
||||||
@@ -580,7 +664,6 @@ func (p *AudioOptions) Render() error {
|
|||||||
return fmt.Errorf("render prompt: %w", err)
|
return fmt.Errorf("render prompt: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Volume row
|
|
||||||
if err := p.RenderTexture(p.textShader, p.VolumeLabel.ID, p.VolumeLabel.Position(), p.VolumeLabel.RawDimensions(), p.VolumeLabel.Color); err != nil {
|
if err := p.RenderTexture(p.textShader, p.VolumeLabel.ID, p.VolumeLabel.Position(), p.VolumeLabel.RawDimensions(), p.VolumeLabel.Color); err != nil {
|
||||||
return fmt.Errorf("render volume label: %w", err)
|
return fmt.Errorf("render volume label: %w", err)
|
||||||
}
|
}
|
||||||
@@ -591,23 +674,52 @@ func (p *AudioOptions) Render() error {
|
|||||||
return fmt.Errorf("render volume percent: %w", err)
|
return fmt.Errorf("render volume percent: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle
|
|
||||||
if err := p.RenderTexture(p.textShader, p.AudioStatusToggle.ID, p.AudioStatusToggle.Position(), p.AudioStatusToggle.RawDimensions(), p.AudioStatusToggle.Color); err != nil {
|
if err := p.RenderTexture(p.textShader, p.AudioStatusToggle.ID, p.AudioStatusToggle.Position(), p.AudioStatusToggle.RawDimensions(), p.AudioStatusToggle.Color); err != nil {
|
||||||
return fmt.Errorf("render audio toggle: %w", err)
|
return fmt.Errorf("render audio toggle: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Audio file list
|
// Section header
|
||||||
for i := range p.audioFileTextures {
|
p.RenderTexture(p.textShader, p.sectionHeader.ID, p.sectionHeader.Position(), p.sectionHeader.RawDimensions(), p.sectionHeader.Color)
|
||||||
|
|
||||||
|
// List background
|
||||||
|
listBgColor := sdl.Color{R: 255, G: 255, B: 255, A: 12}
|
||||||
|
bgPad := int32(16)
|
||||||
|
bgLeft := float32(p.listLeftX - bgPad)
|
||||||
|
bgTop := float32(p.listAreaBottom)
|
||||||
|
bgRight := float32(p.scrollTrackX)
|
||||||
|
bgBottom := float32(p.listAreaTop)
|
||||||
|
p.RenderSolidColorQuad(p.solidShader,
|
||||||
|
mgl32.Vec2{bgLeft, bgTop},
|
||||||
|
mgl32.Vec2{bgRight - bgLeft, bgBottom - bgTop},
|
||||||
|
listBgColor)
|
||||||
|
|
||||||
|
// Audio file list (visible window only)
|
||||||
|
for i := p.scrollOffset; i < p.scrollOffset+p.maxVisible && i < len(p.audioFileTextures); i++ {
|
||||||
item := &p.audioFileTextures[i]
|
item := &p.audioFileTextures[i]
|
||||||
color := colors.White
|
clr := colors.White
|
||||||
if i == p.selectedFileIndex {
|
if i == p.selectedFileIndex {
|
||||||
color = colors.Green
|
clr = colors.Green
|
||||||
}
|
}
|
||||||
if err := p.RenderTexture(p.textShader, item.ID, item.Position(), item.RawDimensions(), color); err != nil {
|
|
||||||
|
if err := p.RenderTexture(p.textShader, item.ID, item.Position(), item.RawDimensions(), clr); err != nil {
|
||||||
return fmt.Errorf("render audio file %d: %w", i, err)
|
return fmt.Errorf("render audio file %d: %w", i, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Selection dot — small filled circle left of the item name
|
||||||
|
if i == p.selectedFileIndex {
|
||||||
|
dotX := float32(item.X) - p.dotSize - 6
|
||||||
|
dotY := float32(item.Y) + float32(item.H)/2 - p.dotSize/2
|
||||||
|
p.RenderSolidColorQuad(p.solidShader,
|
||||||
|
mgl32.Vec2{dotX, dotY},
|
||||||
|
mgl32.Vec2{p.dotSize, p.dotSize},
|
||||||
|
colors.Green)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scrollbar
|
||||||
|
p.renderScrollbar()
|
||||||
|
|
||||||
// Spinning triangle cursor — always on top
|
// Spinning triangle cursor — always on top
|
||||||
if err := p.renderCursor(); err != nil {
|
if err := p.renderCursor(); err != nil {
|
||||||
return fmt.Errorf("render cursor: %w", err)
|
return fmt.Errorf("render cursor: %w", err)
|
||||||
@@ -616,14 +728,13 @@ func (p *AudioOptions) Render() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Destroy ───────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
func (p *AudioOptions) Destroy() error {
|
func (p *AudioOptions) Destroy() error {
|
||||||
gl.DeleteTextures(1, &p.title.ID)
|
gl.DeleteTextures(1, &p.title.ID)
|
||||||
gl.DeleteTextures(1, &p.prompt.ID)
|
gl.DeleteTextures(1, &p.prompt.ID)
|
||||||
gl.DeleteTextures(1, &p.VolumeLabel.ID)
|
gl.DeleteTextures(1, &p.VolumeLabel.ID)
|
||||||
gl.DeleteTextures(1, &p.volumePercent.ID)
|
gl.DeleteTextures(1, &p.volumePercent.ID)
|
||||||
gl.DeleteTextures(1, &p.AudioStatusToggle.ID)
|
gl.DeleteTextures(1, &p.AudioStatusToggle.ID)
|
||||||
|
gl.DeleteTextures(1, &p.sectionHeader.ID)
|
||||||
for i := range p.audioFileTextures {
|
for i := range p.audioFileTextures {
|
||||||
gl.DeleteTextures(1, &p.audioFileTextures[i].ID)
|
gl.DeleteTextures(1, &p.audioFileTextures[i].ID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
"rhemawavetech.com/rhemawave/internal/shaderManager"
|
"rhemawavetech.com/rhemawave/internal/shaderManager"
|
||||||
)
|
)
|
||||||
|
|
||||||
var GitVersion = "07112026-1710"
|
var GitVersion = "07292026_1445"
|
||||||
|
|
||||||
type Home struct {
|
type Home struct {
|
||||||
Base
|
Base
|
||||||
|
|||||||
Executable
+71
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "=== Debian dev environment setup ==="
|
||||||
|
|
||||||
|
# Update package index
|
||||||
|
sudo apt-get update
|
||||||
|
|
||||||
|
# --- Core dev & build tools ---
|
||||||
|
sudo apt-get install -y \
|
||||||
|
curl \
|
||||||
|
g++ \
|
||||||
|
gcc \
|
||||||
|
git \
|
||||||
|
make \
|
||||||
|
cmake \
|
||||||
|
ninja-build \
|
||||||
|
pkg-config
|
||||||
|
|
||||||
|
# --- SDL2 and all official extension libraries ---
|
||||||
|
sudo apt-get install -y \
|
||||||
|
libsdl2-dev \
|
||||||
|
libsdl2-mixer-dev \
|
||||||
|
libsdl2-net-dev \
|
||||||
|
libsdl2-ttf-dev \
|
||||||
|
libsdl2-image-dev \
|
||||||
|
libsdl2-gfx-dev
|
||||||
|
|
||||||
|
# --- SDL3 (available in Debian testing/sid, but not bookworm) ---
|
||||||
|
# If this fails, SDL3 isn't in your Debian release yet.
|
||||||
|
# See https://github.com/libsdl-org/SDL for building from source.
|
||||||
|
sudo apt-get install -y libsdl3-dev || echo "SDL3 not found in apt (expected on older Debian releases)"
|
||||||
|
|
||||||
|
# --- Go ---
|
||||||
|
if ! command -v go &>/dev/null; then
|
||||||
|
echo "Installing Go..."
|
||||||
|
GO_VERSION=$(curl -sL https://go.dev/VERSION?m=text | head -1)
|
||||||
|
GO_TAR="${GO_VERSION}.linux-amd64.tar.gz"
|
||||||
|
curl -sLO "https://go.dev/dl/${GO_TAR}"
|
||||||
|
sudo rm -rf /usr/local/go
|
||||||
|
sudo tar -C /usr/local -xzf "${GO_TAR}"
|
||||||
|
rm "${GO_TAR}"
|
||||||
|
export PATH=$PATH:/usr/local/go/bin
|
||||||
|
if ! grep -q '/usr/local/go/bin' "$HOME/.profile" 2>/dev/null; then
|
||||||
|
echo 'export PATH=$PATH:/usr/local/go/bin' >> "$HOME/.profile"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Go already installed: $(go version)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Nix (multi-user install) ---
|
||||||
|
if ! command -v nix &>/dev/null; then
|
||||||
|
echo "Installing Nix..."
|
||||||
|
sh <(curl -sL https://nixos.org/nix/install) --daemon
|
||||||
|
echo "Nix installed. You may need to log out and back in, or run:"
|
||||||
|
echo " . /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh"
|
||||||
|
else
|
||||||
|
echo "Nix already installed: $(nix --version)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Setup complete ==="
|
||||||
|
echo "Installed: gcc, g++, make, cmake, ninja, pkg-config"
|
||||||
|
echo "Installed: SDL2 + mixer/net/ttf/image/gfx"
|
||||||
|
echo "Installed: Go $(go version 2>/dev/null || echo '(check PATH)')"
|
||||||
|
echo "Installed: Nix $(nix --version 2>/dev/null || echo '(restart shell)')"
|
||||||
|
if dpkg -l libsdl3-dev &>/dev/null; then
|
||||||
|
echo "Installed: SDL3"
|
||||||
|
else
|
||||||
|
echo "SDL3: not in apt for this release (install from https://github.com/libsdl-org/SDL)"
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user