diff options
-rw-r--r-- | CMakeLists.txt | 2 | ||||
-rw-r--r-- | src/audio/player.c | 29 | ||||
-rw-r--r-- | src/audio/player.h | 11 | ||||
-rw-r--r-- | src/ui/documentwidget.c | 180 | ||||
-rw-r--r-- | src/ui/playerui.c | 184 | ||||
-rw-r--r-- | src/ui/playerui.h | 44 | ||||
-rw-r--r-- | src/ui/text.h | 9 | ||||
-rw-r--r-- | src/ui/window.c | 2 |
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 | ||
266 | void init_Decoder(iDecoder *d, iInputBuf *input, const iContentSpec *spec) { | 267 | void 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 | ||
469 | void deinit_Player(iPlayer *d) { | 474 | void 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 | ||
489 | float volume_Player(const iPlayer *d) { | ||
490 | return d->volume; | ||
491 | } | ||
492 | |||
484 | void updateSourceData_Player(iPlayer *d, const iString *mimeType, const iBlock *data, | 493 | void 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 | ||
561 | void 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 | |||
568 | void setFlags_Player(iPlayer *d, int flags, iBool set) { | ||
569 | iChangeFlags(d->flags, flags, set); | ||
570 | } | ||
571 | |||
572 | int flags_Player(const iPlayer *d) { | ||
573 | return d->flags; | ||
574 | } | ||
575 | |||
551 | float time_Player(const iPlayer *d) { | 576 | float 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 | ||
36 | enum iPlayerFlag { | ||
37 | adjustingVolume_PlayerFlag = iBit(1), | ||
38 | volumeGrabbed_PlayerFlag = iBit(2), | ||
39 | }; | ||
40 | |||
36 | void updateSourceData_Player (iPlayer *, const iString *mimeType, const iBlock *data, | 41 | void updateSourceData_Player (iPlayer *, const iString *mimeType, const iBlock *data, |
37 | enum iPlayerUpdate update); | 42 | enum iPlayerUpdate update); |
38 | 43 | ||
39 | iBool start_Player (iPlayer *); | 44 | iBool start_Player (iPlayer *); |
40 | void setPaused_Player (iPlayer *, iBool isPaused); | ||
41 | void stop_Player (iPlayer *); | 45 | void stop_Player (iPlayer *); |
46 | void setPaused_Player (iPlayer *, iBool isPaused); | ||
47 | void setVolume_Player (iPlayer *, float volume); | ||
48 | void setFlags_Player (iPlayer *, int flags, iBool set); | ||
42 | 49 | ||
50 | int flags_Player (const iPlayer *); | ||
43 | iBool isStarted_Player (const iPlayer *); | 51 | iBool isStarted_Player (const iPlayer *); |
44 | iBool isPaused_Player (const iPlayer *); | 52 | iBool isPaused_Player (const iPlayer *); |
53 | float volume_Player (const iPlayer *); | ||
45 | float time_Player (const iPlayer *); | 54 | float time_Player (const iPlayer *); |
46 | float duration_Player (const iPlayer *); | 55 | float duration_Player (const iPlayer *); |
47 | float streamProgress_Player (const iPlayer *); /* normalized 0...1 */ | 56 | float 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 | ||
1535 | iDeclareType(PlayerUI) | ||
1536 | |||
1537 | struct Impl_PlayerUI { | ||
1538 | const iPlayer *player; | ||
1539 | iRect bounds; | ||
1540 | iRect playPauseRect; | ||
1541 | iRect rewindRect; | ||
1542 | iRect scrubberRect; | ||
1543 | iRect menuRect; | ||
1544 | }; | ||
1545 | |||
1546 | static 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 | |||
1556 | static 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 | |||
1573 | static 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 | |||
1599 | static 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 | |||
1640 | static iRect audioPlayerRect_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run) { | 1539 | static 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 | ||
1544 | static 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 | |||
1645 | static iBool processAudioPlayerEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { | 1565 | static 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 | |||
2563 | void serializeState_DocumentWidget(const iDocumentWidget *d, iStream *outs) { | 2523 | void 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 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. 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 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY 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 | ||
21 | SOFTWARE, 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 | |||
28 | static 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 | |||
41 | void 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 | |||
58 | static 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 | |||
75 | static 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 | |||
101 | void 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 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. 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 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY 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 | ||
21 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ | ||
22 | |||
23 | #pragma once | ||
24 | |||
25 | #include <the_Foundation/rect.h> | ||
26 | |||
27 | iDeclareType(Paint) | ||
28 | iDeclareType(Player) | ||
29 | iDeclareType(PlayerUI) | ||
30 | |||
31 | struct 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 | |||
43 | void init_PlayerUI (iPlayerUI *, const iPlayer *player, iRect bounds); | ||
44 | void 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. */ { |