summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt2
-rw-r--r--src/audio/player.c29
-rw-r--r--src/audio/player.h11
-rw-r--r--src/ui/documentwidget.c180
-rw-r--r--src/ui/playerui.c184
-rw-r--r--src/ui/playerui.h44
-rw-r--r--src/ui/text.h9
-rw-r--r--src/ui/window.c2
8 files changed, 343 insertions, 118 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e69fe3cb..8faeeb73 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -134,6 +134,8 @@ set (SOURCES
134 src/ui/metrics.h 134 src/ui/metrics.h
135 src/ui/paint.c 135 src/ui/paint.c
136 src/ui/paint.h 136 src/ui/paint.h
137 src/ui/playerui.c
138 src/ui/playerui.h
137 src/ui/scrollwidget.c 139 src/ui/scrollwidget.c
138 src/ui/scrollwidget.h 140 src/ui/scrollwidget.h
139 src/ui/sidebarwidget.c 141 src/ui/sidebarwidget.c
diff --git a/src/audio/player.c b/src/audio/player.c
index 226e0662..b2a6ae15 100644
--- a/src/audio/player.c
+++ b/src/audio/player.c
@@ -203,10 +203,11 @@ static enum iDecoderStatus decodeVorbis_Decoder_(iDecoder *d, iRanges inputRange
203 else continue; 203 else continue;
204 } 204 }
205 /* Apply gain. */ { 205 /* Apply gain. */ {
206 const float gain = d->gain;
206 float sample[2]; 207 float sample[2];
207 for (size_t i = 0; i < (size_t) count; ++i) { 208 for (size_t i = 0; i < (size_t) count; ++i) {
208 for (size_t chan = 0; chan < d->output.numChannels; chan++) { 209 for (size_t chan = 0; chan < d->output.numChannels; chan++) {
209 sample[chan] = samples[chan][i] * d->gain; 210 sample[chan] = samples[chan][i] * gain;
210 } 211 }
211 pushBack_Array(&d->pendingOutput, sample); 212 pushBack_Array(&d->pendingOutput, sample);
212 } 213 }
@@ -265,7 +266,7 @@ static iThreadResult run_Decoder_(iThread *thread) {
265 266
266void init_Decoder(iDecoder *d, iInputBuf *input, const iContentSpec *spec) { 267void init_Decoder(iDecoder *d, iInputBuf *input, const iContentSpec *spec) {
267 d->type = spec->type; 268 d->type = spec->type;
268 d->gain = 0.5f; 269 d->gain = 1.0f;
269 d->input = input; 270 d->input = input;
270 d->inputPos = spec->inputStartPos; 271 d->inputPos = spec->inputStartPos;
271 d->inputFormat = spec->inputFormat; 272 d->inputFormat = spec->inputFormat;
@@ -305,6 +306,8 @@ struct Impl_Player {
305 SDL_AudioSpec spec; 306 SDL_AudioSpec spec;
306 SDL_AudioDeviceID device; 307 SDL_AudioDeviceID device;
307 iString mime; 308 iString mime;
309 float volume;
310 int flags;
308 iInputBuf * data; 311 iInputBuf * data;
309 iDecoder * decoder; 312 iDecoder * decoder;
310}; 313};
@@ -464,6 +467,8 @@ void init_Player(iPlayer *d) {
464 d->device = 0; 467 d->device = 0;
465 d->decoder = NULL; 468 d->decoder = NULL;
466 d->data = new_InputBuf(); 469 d->data = new_InputBuf();
470 d->volume = 1.0f;
471 d->flags = 0;
467} 472}
468 473
469void deinit_Player(iPlayer *d) { 474void deinit_Player(iPlayer *d) {
@@ -481,6 +486,10 @@ iBool isPaused_Player(const iPlayer *d) {
481 return SDL_GetAudioDeviceStatus(d->device) == SDL_AUDIO_PAUSED; 486 return SDL_GetAudioDeviceStatus(d->device) == SDL_AUDIO_PAUSED;
482} 487}
483 488
489float volume_Player(const iPlayer *d) {
490 return d->volume;
491}
492
484void updateSourceData_Player(iPlayer *d, const iString *mimeType, const iBlock *data, 493void updateSourceData_Player(iPlayer *d, const iString *mimeType, const iBlock *data,
485 enum iPlayerUpdate update) { 494 enum iPlayerUpdate update) {
486 /* TODO: Add MIME as argument */ 495 /* TODO: Add MIME as argument */
@@ -527,6 +536,7 @@ iBool start_Player(iPlayer *d) {
527 return iFalse; 536 return iFalse;
528 } 537 }
529 d->decoder = new_Decoder(d->data, &content); 538 d->decoder = new_Decoder(d->data, &content);
539 d->decoder->gain = d->volume;
530 SDL_PauseAudioDevice(d->device, SDL_FALSE); 540 SDL_PauseAudioDevice(d->device, SDL_FALSE);
531 return iTrue; 541 return iTrue;
532} 542}
@@ -548,6 +558,21 @@ void stop_Player(iPlayer *d) {
548 } 558 }
549} 559}
550 560
561void setVolume_Player(iPlayer *d, float volume) {
562 d->volume = iClamp(volume, 0, 1);
563 if (d->decoder) {
564 d->decoder->gain = d->volume;
565 }
566}
567
568void setFlags_Player(iPlayer *d, int flags, iBool set) {
569 iChangeFlags(d->flags, flags, set);
570}
571
572int flags_Player(const iPlayer *d) {
573 return d->flags;
574}
575
551float time_Player(const iPlayer *d) { 576float time_Player(const iPlayer *d) {
552 if (!d->decoder) return 0; 577 if (!d->decoder) return 0;
553 return (float) ((double) d->decoder->currentSample / (double) d->spec.freq); 578 return (float) ((double) d->decoder->currentSample / (double) d->spec.freq);
diff --git a/src/audio/player.h b/src/audio/player.h
index 720f2d78..268188aa 100644
--- a/src/audio/player.h
+++ b/src/audio/player.h
@@ -33,15 +33,24 @@ enum iPlayerUpdate {
33 complete_PlayerUpdate, 33 complete_PlayerUpdate,
34}; 34};
35 35
36enum iPlayerFlag {
37 adjustingVolume_PlayerFlag = iBit(1),
38 volumeGrabbed_PlayerFlag = iBit(2),
39};
40
36void updateSourceData_Player (iPlayer *, const iString *mimeType, const iBlock *data, 41void updateSourceData_Player (iPlayer *, const iString *mimeType, const iBlock *data,
37 enum iPlayerUpdate update); 42 enum iPlayerUpdate update);
38 43
39iBool start_Player (iPlayer *); 44iBool start_Player (iPlayer *);
40void setPaused_Player (iPlayer *, iBool isPaused);
41void stop_Player (iPlayer *); 45void stop_Player (iPlayer *);
46void setPaused_Player (iPlayer *, iBool isPaused);
47void setVolume_Player (iPlayer *, float volume);
48void setFlags_Player (iPlayer *, int flags, iBool set);
42 49
50int flags_Player (const iPlayer *);
43iBool isStarted_Player (const iPlayer *); 51iBool isStarted_Player (const iPlayer *);
44iBool isPaused_Player (const iPlayer *); 52iBool isPaused_Player (const iPlayer *);
53float volume_Player (const iPlayer *);
45float time_Player (const iPlayer *); 54float time_Player (const iPlayer *);
46float duration_Player (const iPlayer *); 55float duration_Player (const iPlayer *);
47float streamProgress_Player (const iPlayer *); /* normalized 0...1 */ 56float streamProgress_Player (const iPlayer *); /* normalized 0...1 */
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index e8287751..5adb8bd0 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -34,6 +34,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
34#include "labelwidget.h" 34#include "labelwidget.h"
35#include "media.h" 35#include "media.h"
36#include "paint.h" 36#include "paint.h"
37#include "playerui.h"
37#include "scrollwidget.h" 38#include "scrollwidget.h"
38#include "util.h" 39#include "util.h"
39#include "visbuf.h" 40#include "visbuf.h"
@@ -178,6 +179,8 @@ struct Impl_DocumentWidget {
178 int pageMargin; 179 int pageMargin;
179 iPtrArray visibleLinks; 180 iPtrArray visibleLinks;
180 iPtrArray visiblePlayers; /* currently playing audio */ 181 iPtrArray visiblePlayers; /* currently playing audio */
182 const iGmRun * grabbedPlayer; /* currently adjusting volume in a player */
183 float grabbedStartVolume;
181 const iGmRun * hoverLink; 184 const iGmRun * hoverLink;
182 const iGmRun * contextLink; 185 const iGmRun * contextLink;
183 iBool noHoverWhileScrolling; 186 iBool noHoverWhileScrolling;
@@ -244,6 +247,7 @@ void init_DocumentWidget(iDocumentWidget *d) {
244 init_Block(&d->sourceContent, 0); 247 init_Block(&d->sourceContent, 0);
245 init_PtrArray(&d->visibleLinks); 248 init_PtrArray(&d->visibleLinks);
246 init_PtrArray(&d->visiblePlayers); 249 init_PtrArray(&d->visiblePlayers);
250 d->grabbedPlayer = NULL;
247 init_Click(&d->click, d, SDL_BUTTON_LEFT); 251 init_Click(&d->click, d, SDL_BUTTON_LEFT);
248 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); 252 addChild_Widget(w, iClob(d->scroll = new_ScrollWidget()));
249 d->menu = NULL; /* created when clicking */ 253 d->menu = NULL; /* created when clicking */
@@ -1532,116 +1536,32 @@ static size_t visibleLinkOrdinal_DocumentWidget_(const iDocumentWidget *d, iGmLi
1532 return iInvalidPos; 1536 return iInvalidPos;
1533} 1537}
1534 1538
1535iDeclareType(PlayerUI)
1536
1537struct Impl_PlayerUI {
1538 const iPlayer *player;
1539 iRect bounds;
1540 iRect playPauseRect;
1541 iRect rewindRect;
1542 iRect scrubberRect;
1543 iRect menuRect;
1544};
1545
1546static void init_PlayerUI(iPlayerUI *d, const iPlayer *player, iRect bounds) {
1547 d->player = player;
1548 d->bounds = bounds;
1549 const int height = height_Rect(bounds);
1550 d->playPauseRect = (iRect){ addX_I2(topLeft_Rect(bounds), gap_UI / 2), init_I2(3 * height / 2, height) };
1551 d->rewindRect = (iRect){ topRight_Rect(d->playPauseRect), init1_I2(height) };
1552 d->menuRect = (iRect){ addX_I2(topRight_Rect(bounds), -height - gap_UI / 2), init1_I2(height) };
1553 d->scrubberRect = initCorners_Rect(topRight_Rect(d->rewindRect), bottomLeft_Rect(d->menuRect));
1554}
1555
1556static void drawPlayerButton_(iPaint *p, iRect rect, const char *label) {
1557 const iInt2 mouse = mouseCoord_Window(get_Window());
1558 const iBool isHover = contains_Rect(rect, mouse);
1559 const iBool isPressed = isHover && (SDL_GetMouseState(NULL, NULL) & SDL_BUTTON_LEFT) != 0;
1560 const int frame = (isPressed ? uiTextCaution_ColorId : isHover ? uiHeading_ColorId : uiAnnotation_ColorId);
1561 iRect frameRect = shrunk_Rect(rect, init_I2(gap_UI / 2, gap_UI));
1562 drawRect_Paint(p, frameRect, frame);
1563 if (isPressed) {
1564 fillRect_Paint(
1565 p,
1566 adjusted_Rect(shrunk_Rect(frameRect, divi_I2(gap2_UI, 2)), zero_I2(), one_I2()),
1567 frame);
1568 }
1569 const int fg = isPressed ? (permanent_ColorId | uiBackground_ColorId) : uiHeading_ColorId;
1570 drawCentered_Text(uiContent_FontId, frameRect, iTrue, fg, "%s", label);
1571}
1572
1573static int drawSevenSegmentTime_(iInt2 pos, int color, int align, int seconds) { /* returns width */
1574 const uint32_t sevenSegmentDigit = 0x1fbf0;
1575 const int hours = seconds / 3600;
1576 const int mins = (seconds / 60) % 60;
1577 const int secs = seconds % 60;
1578 const int font = uiLabel_FontId;
1579 iString num;
1580 init_String(&num);
1581 if (hours) {
1582 appendChar_String(&num, sevenSegmentDigit + (hours % 10));
1583 appendChar_String(&num, ':');
1584 }
1585 appendChar_String(&num, sevenSegmentDigit + (mins / 10) % 10);
1586 appendChar_String(&num, sevenSegmentDigit + (mins % 10));
1587 appendChar_String(&num, ':');
1588 appendChar_String(&num, sevenSegmentDigit + (secs / 10) % 10);
1589 appendChar_String(&num, sevenSegmentDigit + (secs % 10));
1590 iInt2 size = advanceRange_Text(font, range_String(&num));
1591 if (align == right_Alignment) {
1592 pos.x -= size.x;
1593 }
1594 drawRange_Text(font, pos, color, range_String(&num));
1595 deinit_String(&num);
1596 return size.x;
1597}
1598
1599static void draw_PlayerUI(iPlayerUI *d, iPaint *p) {
1600 const int playerBackground_ColorId = uiBackground_ColorId;
1601 const int playerFrame_ColorId = uiSeparator_ColorId;
1602 fillRect_Paint(p, d->bounds, playerBackground_ColorId);
1603 drawRect_Paint(p, d->bounds, playerFrame_ColorId);
1604 drawPlayerButton_(p, d->playPauseRect, isPaused_Player(d->player) ? "\U0001f782" : "\u23f8");
1605 drawPlayerButton_(p, d->rewindRect, "\u23ee");
1606 drawPlayerButton_(p, d->menuRect, "\U0001d362");
1607 const int hgt = lineHeight_Text(uiLabel_FontId);
1608 const int yMid = mid_Rect(d->scrubberRect).y;
1609 const float playTime = time_Player(d->player);
1610 const float totalTime = duration_Player(d->player);
1611 const int bright = uiHeading_ColorId;
1612 const int dim = uiAnnotation_ColorId;
1613 int leftWidth = drawSevenSegmentTime_(
1614 init_I2(left_Rect(d->scrubberRect) + 2 * gap_UI, yMid - hgt / 2),
1615 isPaused_Player(d->player) ? dim : bright,
1616 left_Alignment,
1617 iRound(playTime));
1618 int rightWidth = 0;
1619 if (totalTime > 0) {
1620 rightWidth =
1621 drawSevenSegmentTime_(init_I2(right_Rect(d->scrubberRect) - 2 * gap_UI, yMid - hgt / 2),
1622 dim,
1623 right_Alignment,
1624 iRound(totalTime));
1625 }
1626 /* Scrubber. */
1627 const int s1 = left_Rect(d->scrubberRect) + leftWidth + 6 * gap_UI;
1628 const int s2 = right_Rect(d->scrubberRect) - rightWidth - 6 * gap_UI;
1629 const float normPos = totalTime > 0 ? playTime / totalTime : 0.0f;
1630 const int part = (s2 - s1) * normPos;
1631 const int scrubMax = (s2 - s1) * streamProgress_Player(d->player);
1632 drawHLine_Paint(p, init_I2(s1, yMid), part, bright);
1633 drawHLine_Paint(p, init_I2(s1 + part, yMid), scrubMax - part, dim);
1634 draw_Text(uiLabel_FontId,
1635 init_I2(s1 * (1.0f - normPos) + s2 * normPos - hgt / 3, yMid - hgt / 2),
1636 bright,
1637 "\u23fa");
1638}
1639
1640static iRect audioPlayerRect_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run) { 1539static iRect audioPlayerRect_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run) {
1641 const iRect docBounds = documentBounds_DocumentWidget_(d); 1540 const iRect docBounds = documentBounds_DocumentWidget_(d);
1642 return moved_Rect(run->bounds, addY_I2(topLeft_Rect(docBounds), -d->scrollY)); 1541 return moved_Rect(run->bounds, addY_I2(topLeft_Rect(docBounds), -d->scrollY));
1643} 1542}
1644 1543
1544static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *run) {
1545 if (run) {
1546 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), run->audioId);
1547 setFlags_Player(plr, volumeGrabbed_PlayerFlag, iTrue);
1548 d->grabbedStartVolume = volume_Player(plr);
1549 d->grabbedPlayer = run;
1550 refresh_Widget(d);
1551 }
1552 else if (d->grabbedPlayer) {
1553 setFlags_Player(
1554 audioPlayer_Media(media_GmDocument(d->doc), d->grabbedPlayer->audioId),
1555 volumeGrabbed_PlayerFlag,
1556 iFalse);
1557 d->grabbedPlayer = NULL;
1558 refresh_Widget(d);
1559 }
1560 else {
1561 iAssert(iFalse);
1562 }
1563}
1564
1645static iBool processAudioPlayerEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { 1565static iBool processAudioPlayerEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) {
1646 if (ev->type != SDL_MOUSEBUTTONDOWN && ev->type != SDL_MOUSEBUTTONUP && 1566 if (ev->type != SDL_MOUSEBUTTONDOWN && ev->type != SDL_MOUSEBUTTONUP &&
1647 ev->type != SDL_MOUSEMOTION) { 1567 ev->type != SDL_MOUSEMOTION) {
@@ -1652,18 +1572,32 @@ static iBool processAudioPlayerEvents_DocumentWidget_(iDocumentWidget *d, const
1652 return iFalse; 1572 return iFalse;
1653 } 1573 }
1654 } 1574 }
1575 if (d->grabbedPlayer) {
1576 /* Updated in the drag. */
1577 return iFalse;
1578 }
1655 const iInt2 mouse = init_I2(ev->button.x, ev->button.y); 1579 const iInt2 mouse = init_I2(ev->button.x, ev->button.y);
1656 iConstForEach(PtrArray, i, &d->visiblePlayers) { 1580 iConstForEach(PtrArray, i, &d->visiblePlayers) {
1657 const iGmRun *run = i.ptr; 1581 const iGmRun *run = i.ptr;
1658 const iRect rect = audioPlayerRect_DocumentWidget_(d, run); 1582 const iRect rect = audioPlayerRect_DocumentWidget_(d, run);
1583 iPlayer * plr = audioPlayer_Media(media_GmDocument(d->doc), run->audioId);
1659 if (contains_Rect(rect, mouse)) { 1584 if (contains_Rect(rect, mouse)) {
1585 iPlayerUI ui;
1586 init_PlayerUI(&ui, plr, rect);
1660 if (ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEMOTION) { 1587 if (ev->type == SDL_MOUSEBUTTONDOWN || ev->type == SDL_MOUSEMOTION) {
1588 if (ev->type == SDL_MOUSEBUTTONDOWN &&
1589 flags_Player(plr) & adjustingVolume_PlayerFlag &&
1590 contains_Rect(adjusted_Rect(ui.volumeAdjustRect,
1591 zero_I2(),
1592 init_I2(-height_Rect(ui.volumeAdjustRect), 0)),
1593 mouse)) {
1594 setGrabbedPlayer_DocumentWidget_(d, run);
1595 processEvent_Click(&d->click, ev);
1596 /* The rest is done in the DocumentWidget click responder. */
1597 }
1661 refresh_Widget(d); 1598 refresh_Widget(d);
1662 return iTrue; 1599 return iTrue;
1663 } 1600 }
1664 iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), run->audioId);
1665 iPlayerUI ui;
1666 init_PlayerUI(&ui, plr, rect);
1667 if (contains_Rect(ui.playPauseRect, mouse)) { 1601 if (contains_Rect(ui.playPauseRect, mouse)) {
1668 setPaused_Player(plr, !isPaused_Player(plr)); 1602 setPaused_Player(plr, !isPaused_Player(plr));
1669 animatePlayingAudio_DocumentWidget_(d); 1603 animatePlayingAudio_DocumentWidget_(d);
@@ -1678,6 +1612,13 @@ static iBool processAudioPlayerEvents_DocumentWidget_(iDocumentWidget *d, const
1678 refresh_Widget(d); 1612 refresh_Widget(d);
1679 return iTrue; 1613 return iTrue;
1680 } 1614 }
1615 else if (contains_Rect(ui.volumeRect, mouse)) {
1616 setFlags_Player(plr,
1617 adjustingVolume_PlayerFlag,
1618 !(flags_Player(plr) & adjustingVolume_PlayerFlag));
1619 refresh_Widget(d);
1620 return iTrue;
1621 }
1681 else if (contains_Rect(ui.menuRect, mouse)) { 1622 else if (contains_Rect(ui.menuRect, mouse)) {
1682 /* TODO: Add menu items for: 1623 /* TODO: Add menu items for:
1683 - output device 1624 - output device
@@ -1933,6 +1874,16 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1933 d->selecting = iFalse; 1874 d->selecting = iFalse;
1934 return iTrue; 1875 return iTrue;
1935 case drag_ClickResult: { 1876 case drag_ClickResult: {
1877 if (d->grabbedPlayer) {
1878 iPlayer *plr =
1879 audioPlayer_Media(media_GmDocument(d->doc), d->grabbedPlayer->audioId);
1880 iPlayerUI ui;
1881 init_PlayerUI(&ui, plr, audioPlayerRect_DocumentWidget_(d, d->grabbedPlayer));
1882 float off = (float) delta_Click(&d->click).x / (float) width_Rect(ui.volumeSlider);
1883 setVolume_Player(plr, d->grabbedStartVolume + off);
1884 refresh_Widget(w);
1885 return iTrue;
1886 }
1936 /* Begin selecting a range of text. */ 1887 /* Begin selecting a range of text. */
1937 if (!d->selecting) { 1888 if (!d->selecting) {
1938 setFocus_Widget(NULL); /* TODO: Focus this document? */ 1889 setFocus_Widget(NULL); /* TODO: Focus this document? */
@@ -1955,6 +1906,10 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
1955 return iTrue; 1906 return iTrue;
1956 } 1907 }
1957 case finished_ClickResult: 1908 case finished_ClickResult:
1909 if (d->grabbedPlayer) {
1910 setGrabbedPlayer_DocumentWidget_(d, NULL);
1911 return iTrue;
1912 }
1958 if (isVisible_Widget(d->menu)) { 1913 if (isVisible_Widget(d->menu)) {
1959 closeMenu_Widget(d->menu); 1914 closeMenu_Widget(d->menu);
1960 } 1915 }
@@ -2029,6 +1984,10 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e
2029 return iTrue; 1984 return iTrue;
2030 case double_ClickResult: 1985 case double_ClickResult:
2031 case aborted_ClickResult: 1986 case aborted_ClickResult:
1987 if (d->grabbedPlayer) {
1988 setGrabbedPlayer_DocumentWidget_(d, NULL);
1989 return iTrue;
1990 }
2032 return iTrue; 1991 return iTrue;
2033 default: 1992 default:
2034 break; 1993 break;
@@ -2560,6 +2519,7 @@ const iString *bookmarkTitle_DocumentWidget(const iDocumentWidget *d) {
2560 return collect_String(joinCStr_StringArray(title, " \u2014 ")); 2519 return collect_String(joinCStr_StringArray(title, " \u2014 "));
2561} 2520}
2562 2521
2522
2563void serializeState_DocumentWidget(const iDocumentWidget *d, iStream *outs) { 2523void serializeState_DocumentWidget(const iDocumentWidget *d, iStream *outs) {
2564 serialize_Model(&d->mod, outs); 2524 serialize_Model(&d->mod, outs);
2565} 2525}
diff --git a/src/ui/playerui.c b/src/ui/playerui.c
new file mode 100644
index 00000000..fadbc2da
--- /dev/null
+++ b/src/ui/playerui.c
@@ -0,0 +1,184 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
23#include "playerui.h"
24#include "audio/player.h"
25#include "paint.h"
26#include "util.h"
27
28static const char *volumeChar_(float volume) {
29 if (volume <= 0) {
30 return "\U0001f507";
31 }
32 if (volume < 0.4f) {
33 return "\U0001f508";
34 }
35 if (volume < 0.8f) {
36 return "\U0001f509";
37 }
38 return "\U0001f50a";
39}
40
41void init_PlayerUI(iPlayerUI *d, const iPlayer *player, iRect bounds) {
42 d->player = player;
43 d->bounds = bounds;
44 const int height = height_Rect(bounds);
45 d->playPauseRect = (iRect){ addX_I2(topLeft_Rect(bounds), gap_UI / 2), init_I2(3 * height / 2, height) };
46 d->rewindRect = (iRect){ topRight_Rect(d->playPauseRect), init1_I2(height) };
47 d->menuRect = (iRect){ addX_I2(topRight_Rect(bounds), -height - gap_UI / 2), init1_I2(height) };
48 d->volumeRect = (iRect){ addX_I2(topLeft_Rect(d->menuRect), -height), init1_I2(height) };
49 d->volumeAdjustRect = d->volumeRect;
50 adjustEdges_Rect(&d->volumeAdjustRect, 0, 0, 0, -35 * gap_UI);
51 d->scrubberRect = initCorners_Rect(topRight_Rect(d->rewindRect), bottomLeft_Rect(d->volumeRect));
52 /* Volume slider. */ {
53 d->volumeSlider = shrunk_Rect(d->volumeAdjustRect, init_I2(gap_UI / 2, gap_UI));
54 adjustEdges_Rect(&d->volumeSlider, 0, -width_Rect(d->volumeRect) - 2 * gap_UI, 0, 5 * gap_UI);
55 }
56}
57
58static void drawPlayerButton_(iPaint *p, iRect rect, const char *label, int font) {
59 const iInt2 mouse = mouseCoord_Window(get_Window());
60 const iBool isHover = contains_Rect(rect, mouse);
61 const iBool isPressed = isHover && (SDL_GetMouseState(NULL, NULL) & SDL_BUTTON_LEFT) != 0;
62 const int frame = (isPressed ? uiTextCaution_ColorId : isHover ? uiHeading_ColorId : uiAnnotation_ColorId);
63 iRect frameRect = shrunk_Rect(rect, init_I2(gap_UI / 2, gap_UI));
64 drawRect_Paint(p, frameRect, frame);
65 if (isPressed) {
66 fillRect_Paint(
67 p,
68 adjusted_Rect(shrunk_Rect(frameRect, divi_I2(gap2_UI, 2)), zero_I2(), one_I2()),
69 frame);
70 }
71 const int fg = isPressed ? (permanent_ColorId | uiBackground_ColorId) : uiHeading_ColorId;
72 drawCentered_Text(font, frameRect, iTrue, fg, "%s", label);
73}
74
75static int drawSevenSegmentTime_(iInt2 pos, int color, int align, int seconds) { /* returns width */
76 const uint32_t sevenSegmentDigit = 0x1fbf0;
77 const int hours = seconds / 3600;
78 const int mins = (seconds / 60) % 60;
79 const int secs = seconds % 60;
80 const int font = uiLabel_FontId;
81 iString num;
82 init_String(&num);
83 if (hours) {
84 appendChar_String(&num, sevenSegmentDigit + (hours % 10));
85 appendChar_String(&num, ':');
86 }
87 appendChar_String(&num, sevenSegmentDigit + (mins / 10) % 10);
88 appendChar_String(&num, sevenSegmentDigit + (mins % 10));
89 appendChar_String(&num, ':');
90 appendChar_String(&num, sevenSegmentDigit + (secs / 10) % 10);
91 appendChar_String(&num, sevenSegmentDigit + (secs % 10));
92 iInt2 size = advanceRange_Text(font, range_String(&num));
93 if (align == right_Alignment) {
94 pos.x -= size.x;
95 }
96 drawRange_Text(font, pos, color, range_String(&num));
97 deinit_String(&num);
98 return size.x;
99}
100
101void draw_PlayerUI(iPlayerUI *d, iPaint *p) {
102 const int playerBackground_ColorId = uiBackground_ColorId;
103 const int playerFrame_ColorId = uiSeparator_ColorId;
104 const iBool isAdjusting = (flags_Player(d->player) & adjustingVolume_PlayerFlag) != 0;
105 fillRect_Paint(p, d->bounds, playerBackground_ColorId);
106 drawRect_Paint(p, d->bounds, playerFrame_ColorId);
107 drawPlayerButton_(p,
108 d->playPauseRect,
109 isPaused_Player(d->player) ? "\U0001f782" : "\u23f8",
110 uiContent_FontId);
111 drawPlayerButton_(p, d->rewindRect, "\u23ee", uiContent_FontId);
112 drawPlayerButton_(p, d->menuRect, "\U0001d362", uiContent_FontId);
113 if (!isAdjusting) {
114 drawPlayerButton_(
115 p, d->volumeRect, volumeChar_(volume_Player(d->player)), uiContentSymbols_FontId);
116 }
117 const int hgt = lineHeight_Text(uiLabel_FontId);
118 const int yMid = mid_Rect(d->scrubberRect).y;
119 const float playTime = time_Player(d->player);
120 const float totalTime = duration_Player(d->player);
121 const int bright = uiHeading_ColorId;
122 const int dim = uiAnnotation_ColorId;
123 int leftWidth = drawSevenSegmentTime_(
124 init_I2(left_Rect(d->scrubberRect) + 2 * gap_UI, yMid - hgt / 2),
125 isPaused_Player(d->player) ? dim : bright,
126 left_Alignment,
127 iRound(playTime));
128 int rightWidth = 0;
129 if (totalTime > 0) {
130 rightWidth =
131 drawSevenSegmentTime_(init_I2(right_Rect(d->scrubberRect) - 2 * gap_UI, yMid - hgt / 2),
132 dim,
133 right_Alignment,
134 iRound(totalTime));
135 }
136 /* Scrubber. */
137 const int s1 = left_Rect(d->scrubberRect) + leftWidth + 6 * gap_UI;
138 const int s2 = right_Rect(d->scrubberRect) - rightWidth - 6 * gap_UI;
139 const float normPos = totalTime > 0 ? playTime / totalTime : 0.0f;
140 const int part = (s2 - s1) * normPos;
141 const int scrubMax = (s2 - s1) * streamProgress_Player(d->player);
142 drawHLine_Paint(p, init_I2(s1, yMid), part, bright);
143 drawHLine_Paint(p, init_I2(s1 + part, yMid), scrubMax - part, dim);
144 const char *dot = "\u23fa";
145 const int dotWidth = advance_Text(uiLabel_FontId, dot).x;
146 draw_Text(uiLabel_FontId,
147 init_I2(s1 * (1.0f - normPos) + s2 * normPos - dotWidth / 2, yMid - hgt / 2),
148 bright,
149 dot);
150 /* Volume adjustment. */
151 if (isAdjusting) {
152 const iInt2 mouse = mouseCoord_Window(get_Window());
153 const iBool isHover = contains_Rect(d->volumeRect, mouse) &&
154 ~flags_Player(d->player) & volumeGrabbed_PlayerFlag;
155 const iBool isPressed = (SDL_GetMouseState(NULL, NULL) & SDL_BUTTON_LEFT) != 0;
156 iRect adjRect = shrunk_Rect(d->volumeAdjustRect, init_I2(gap_UI / 2, gap_UI));
157 fillRect_Paint(p, adjRect, playerBackground_ColorId);
158 drawRect_Paint(p, adjRect, bright);
159 if (isHover) {
160 fillRect_Paint(
161 p,
162 shrunk_Rect(d->volumeRect, init_I2(gap_UI / 2 + gap_UI / 2, 3 * gap_UI / 2)),
163 isPressed ? uiTextCaution_ColorId : bright);
164 }
165 drawCentered_Text(uiContentSymbols_FontId,
166 d->volumeRect,
167 iTrue,
168 isHover ? playerBackground_ColorId : bright,
169 volumeChar_(volume_Player(d->player)));
170 const int volColor =
171 flags_Player(d->player) & volumeGrabbed_PlayerFlag ? uiTextCaution_ColorId : bright;
172 const int volPart = volume_Player(d->player) * width_Rect(d->volumeSlider);
173 const iInt2 volPos = init_I2(left_Rect(d->volumeSlider), mid_Rect(d->volumeSlider).y);
174 drawHLine_Paint(p, volPos, volPart, volColor);
175 drawHLine_Paint(p,
176 addX_I2(volPos, volPart),
177 width_Rect(d->volumeSlider) - volPart,
178 dim);
179 draw_Text(uiLabel_FontId,
180 init_I2(left_Rect(d->volumeSlider) + volPart - dotWidth / 2, yMid - hgt / 2),
181 volColor,
182 dot);
183 }
184}
diff --git a/src/ui/playerui.h b/src/ui/playerui.h
new file mode 100644
index 00000000..a1f4ca9b
--- /dev/null
+++ b/src/ui/playerui.h
@@ -0,0 +1,44 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
23#pragma once
24
25#include <the_Foundation/rect.h>
26
27iDeclareType(Paint)
28iDeclareType(Player)
29iDeclareType(PlayerUI)
30
31struct Impl_PlayerUI {
32 const iPlayer *player;
33 iRect bounds;
34 iRect playPauseRect;
35 iRect rewindRect;
36 iRect scrubberRect;
37 iRect volumeRect;
38 iRect volumeAdjustRect;
39 iRect volumeSlider;
40 iRect menuRect;
41};
42
43void init_PlayerUI (iPlayerUI *, const iPlayer *player, iRect bounds);
44void draw_PlayerUI (iPlayerUI *, iPaint *p);
diff --git a/src/ui/text.h b/src/ui/text.h
index 2a2ce711..87f69300 100644
--- a/src/ui/text.h
+++ b/src/ui/text.h
@@ -78,10 +78,11 @@ enum iFontId {
78 fromSymbolsToEmojiOffset_FontId = 9, 78 fromSymbolsToEmojiOffset_FontId = 9,
79 79
80 /* UI fonts: */ 80 /* UI fonts: */
81 uiLabel_FontId = default_FontId, 81 uiLabel_FontId = default_FontId,
82 uiShortcuts_FontId = default_FontId, 82 uiShortcuts_FontId = default_FontId,
83 uiInput_FontId = defaultMonospace_FontId, 83 uiInput_FontId = defaultMonospace_FontId,
84 uiContent_FontId = defaultMedium_FontId, 84 uiContent_FontId = defaultMedium_FontId,
85 uiContentSymbols_FontId = defaultMediumSymbols_FontId,
85 /* Document fonts: */ 86 /* Document fonts: */
86 paragraph_FontId = regular_FontId, 87 paragraph_FontId = regular_FontId,
87 firstParagraph_FontId = medium_FontId, 88 firstParagraph_FontId = medium_FontId,
diff --git a/src/ui/window.c b/src/ui/window.c
index 4165b9e0..38ee3941 100644
--- a/src/ui/window.c
+++ b/src/ui/window.c
@@ -543,7 +543,7 @@ void init_Window(iWindow *d, iRect rect) {
543 if (left_Rect(rect) >= 0 || top_Rect(rect) >= 0) { 543 if (left_Rect(rect) >= 0 || top_Rect(rect) >= 0) {
544 SDL_SetWindowPosition(d->win, left_Rect(rect), top_Rect(rect)); 544 SDL_SetWindowPosition(d->win, left_Rect(rect), top_Rect(rect));
545 } 545 }
546 const iInt2 minSize = init_I2(400, 250); 546 const iInt2 minSize = init_I2(425, 250);
547 SDL_SetWindowMinimumSize(d->win, minSize.x, minSize.y); 547 SDL_SetWindowMinimumSize(d->win, minSize.x, minSize.y);
548 SDL_SetWindowTitle(d->win, "Lagrange"); 548 SDL_SetWindowTitle(d->win, "Lagrange");
549 /* Some info. */ { 549 /* Some info. */ {