diff --git a/CMakeLists.txt b/CMakeLists.txt index e596316ca..ed0647302 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,7 +10,7 @@ # Windows for compiling everything, but the_Foundation still lacks # native Win32 implementations for the Socket and Process classes. # - Windows builds should use the SDL 2 library precompiled for native -# Windows (MSVC variant) instead the version from MSYS2 (get it from +# Windows (MSVC variant) instead of the version from MSYS2 (get it from # https://libsdl.org/). To make configuration easier, consider writing # for your personal use a pkg-config sdl2.pc file that uses the Windows # version of the library. @@ -19,18 +19,18 @@ cmake_minimum_required (VERSION 3.9) project (Lagrange - VERSION 0.5.0 - DESCRIPTION "Beautiful Gemini Client" + VERSION 0.6.0 + DESCRIPTION "A Beautiful Gemini Client" LANGUAGES C ) set (COPYRIGHT_YEAR 2020) # Build configuration. -option (ENABLE_MPG123 "Use mpg123 for decoding MPEG audio" ON) -option (ENABLE_X11_SWRENDER "Use software rendering under X11" OFF) -option (ENABLE_KERNING "Enable kerning in font renderer (slower)" ON) -option (ENABLE_RESOURCE_EMBED "Embed resources inside the executable" OFF) -option (ENABLE_WINDOWPOS_FIX "Set position after showing window (workaround for SDL bug)" OFF) +option (ENABLE_MPG123 "Use mpg123 for decoding MPEG audio" ON) +option (ENABLE_X11_SWRENDER "Use software rendering under X11" OFF) +option (ENABLE_KERNING "Enable kerning in font renderer (slower)" ON) +option (ENABLE_RESOURCE_EMBED "Embed resources inside the executable" OFF) +option (ENABLE_WINDOWPOS_FIX "Set position after showing window (workaround for SDL bug)" OFF) include (BuildType.cmake) include (Embed.cmake) @@ -71,6 +71,7 @@ set (EMBED_RESOURCES res/fonts/Literata-Bold-opsz=36.ttf res/fonts/Literata-ExtraLight-opsz=18.ttf res/fonts/Literata-LightItalic-opsz=10.ttf + res/fonts/NanumGothic-Regular.ttf res/fonts/NotoEmoji-Regular.ttf res/fonts/Nunito-ExtraBold.ttf res/fonts/Nunito-ExtraLight.ttf @@ -94,6 +95,7 @@ set (SOURCES src/app.h src/bookmarks.c src/bookmarks.h + src/defs.h src/gmcerts.c src/gmcerts.h src/gmdocument.c @@ -127,6 +129,8 @@ set (SOURCES src/ui/command.h src/ui/documentwidget.c src/ui/documentwidget.h + src/ui/indicatorwidget.c + src/ui/indicatorwidget.h src/ui/listwidget.c src/ui/listwidget.h src/ui/lookupwidget.c diff --git a/README.md b/README.md index 95767aee1..54d84930c 100644 --- a/README.md +++ b/README.md @@ -42,19 +42,44 @@ To install to "/dest/path": This will also install an XDG .desktop file for launching the app. -### macOS-specific notes +### Compiling on macOS When using OpenSSL 1.1.1 from Homebrew, you must add its pkgconfig path to your `PKG_CONFIG_PATH` environment variable, for example: - export PKG_CONFIG_PATH=/usr/local/Cellar/openssl@1.1/1.1.1g/lib/pkgconfig + export PKG_CONFIG_PATH=/usr/local/Cellar/openssl@1.1/1.1.1h/lib/pkgconfig -Also, SDL's trackpad scrolling behavior on macOS is not optimal for regular GUI apps because it emulates a physical mouse wheel. This may change in a future release of SDL, but at least in 2.0.12 a [small patch](https://git.skyjake.fi/skyjake/lagrange/raw/branch/dev/sdl2-macos-mouse-scrolling-patch.diff) is required to allow momentum scrolling to come through as single-pixel mouse wheel events. +Also, SDL's trackpad scrolling behavior on macOS is not optimal for regular GUI apps because it emulates a physical mouse wheel. This may change in a future release of SDL, but at least in 2.0.12 a [small patch](https://git.skyjake.fi/skyjake/lagrange/raw/branch/dev/sdl2-macos-mouse-scrolling-patch.diff) is required to allow momentum scrolling to come through as single-pixel mouse wheel events. Note that SDL comes with an Xcode project; use the "Shared Library" target and check that you are doing a Release build. -### Raspberry Pi notes +### Compiling on Windows + +Windows builds require [MSYS2](https://www.msys2.org). In theory, [Clang](https://clang.llvm.org/docs/MSVCCompatibility.html) or GCC (on [MinGW](http://mingw.org)) could be set up natively on Windows for compiling everything, but the_Foundation still lacks native Win32 implementations for the Socket and Process classes and these are required by Lagrange. + +You should use the SDL 2 library precompiled for native Windows (the MSVC variant) instead of the version from MSYS2 or MinGW. You can download a copy of the SDL binaries from https://libsdl.org/. To make configuration easier in your MSYS2 environment, consider writing a custom sdl2.pc file so `pkg-config` can automatically find the correct version of SDL. Below is an example of what your sdl2.pc might look like: + +``` +prefix=/c/SDK/SDL2-2.0.12/ +arch=x64 +libdir=${prefix}/lib/${arch}/ +incdir=${prefix}/include/ + +Name: sdl2 +Description: Simple DirectMedia Layer +Version: 2.0.12-msvc +Libs: ${libdir}/SDL2.dll -mwindows +Cflags: -I${incdir} +``` + +The *-mwindows* option is particularly important as that specifies the target is a GUI application. Also note that you are linking directly against the Windows DLL — do not use any prebuilt .lib files if available, as those as specific to MSVC. + +`pkg-config` will find your .pc file if it is on `PKG_CONFIG_PATH` or you place it in a system-wide pkgconfig directory. + +Once you have compiled a working binary under MSYS2, there is still an additional step required to allow running it directly from the Windows shell: the shared libraries from MSYS2 must be found either via `PATH` or by copying them to the same directory where `lagrange.exe` is located. + +### Compiling on Raspberry Pi You should use a version of SDL that is compiled to take advantage of the Broadcom VideoCore OpenGL ES hardware. This provides the best performance when running Lagrange in a console. -When running under X11, software rendering is the best choice and in that case the SDL from Raspbian etc. is sufficient. +At present time, OpenGL under X11 on Raspberry Pi is still quite slow/experimental. When running under X11, software rendering is the best choice and the SDL from Raspbian etc. is sufficient. The following build options are recommended on Raspberry Pi: diff --git a/res/about/license.gmi b/res/about/license.gmi index ef92d647e..abd4577d4 100644 --- a/res/about/license.gmi +++ b/res/about/license.gmi @@ -103,6 +103,7 @@ This application uses fonts licensed under the Open Font License. => https://fonts.google.com/specimen/EB+Garamond#license EB Garamond => https://github.com/mozilla/Fira/blob/master/LICENSE Fira Sans, Fira Mono => https://github.com/googlefonts/literata/blob/master/OFL.txt Literata +=> https://github.com/google/fonts/blob/master/ofl/nanumgothic/OFL.txt Nanum Gothic => https://github.com/googlefonts/nunito/blob/master/OFL.txt Nunito => https://github.com/adobe-fonts/source-sans-pro/blob/release/LICENSE.md Source Sans Pro diff --git a/res/about/version.gmi b/res/about/version.gmi index 4ec7ea416..fba5aa55d 100644 --- a/res/about/version.gmi +++ b/res/about/version.gmi @@ -6,6 +6,17 @@ ``` # Release notes +## 0.6 +* Added an indicator to visualize progress of network requests. +* Added new color themes for page content: Colorful Light, Black, Gray, Sepia, High Contrast. +* Added page content color theme selection in Preferences. +* Added quote indicator option: icon or vertical line. +* Added a new font for Korean glyhps. +* Smoother smooth scrolling, making it easier to keep one's eyes on the content throughout the motion. +* Windows: Register Lagrange as a handler of "gemini:" URLs. +* macOS: Fixed glitchy window dragging during audio playback. +* Fixed timestamps of cached pages. + ## 0.5 * Added MP3 support in the audio player (using mpg123). => https://mpg123.org/ mpg123: MPEG audio player and decoder library diff --git a/res/fonts/LICENSE_NanumGothic.txt b/res/fonts/LICENSE_NanumGothic.txt new file mode 100644 index 000000000..69ff40157 --- /dev/null +++ b/res/fonts/LICENSE_NanumGothic.txt @@ -0,0 +1,96 @@ +Copyright (c) 2010, NHN Corporation (http://www.nhncorp.com), +with Reserved Font Name Nanum, Naver Nanum, NanumGothic, Naver +NanumGothic, NanumMyeongjo, Naver NanumMyeongjo, NanumBrush, Naver +NanumBrush, NanumPen, Naver NanumPen. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/res/fonts/NanumGothic-Regular.ttf b/res/fonts/NanumGothic-Regular.ttf new file mode 100644 index 000000000..6e4dd8748 Binary files /dev/null and b/res/fonts/NanumGothic-Regular.ttf differ diff --git a/res/lagrange.rc.in b/res/lagrange.rc.in index 7b9f0f650..51afa4f3a 100644 --- a/res/lagrange.rc.in +++ b/res/lagrange.rc.in @@ -16,7 +16,7 @@ BEGIN BLOCK "040904B0" BEGIN VALUE "CompanyName", "Jaakko Ker\xe4nen\0" - VALUE "FileDescription", "${PROJECT_DESCRIPTION}\0" + VALUE "FileDescription", "Lagrange: ${PROJECT_DESCRIPTION}\0" VALUE "FileVersion", "${PROJECT_VERSION}\0" VALUE "InternalName", "fi.skyjake.lagrange\0" VALUE "LegalCopyright", "(c) ${COPYRIGHT_YEAR} Jaakko Ker\xe4nen\0" diff --git a/src/app.c b/src/app.c index ac970865f..da1061416 100644 --- a/src/app.c +++ b/src/app.c @@ -22,6 +22,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "app.h" #include "bookmarks.h" +#include "defs.h" #include "embedded.h" #include "gmcerts.h" #include "gmdocument.h" @@ -181,9 +182,12 @@ static iString *serializePrefs_App_(const iApp *d) { appendFormat_String(str, "linewidth.set arg:%d\n", d->prefs.lineWidth); appendFormat_String(str, "prefs.biglede.changed arg:%d\n", d->prefs.bigFirstParagraph); appendFormat_String(str, "prefs.sideicon.changed arg:%d\n", d->prefs.sideIcon); + appendFormat_String(str, "quoteicon.set arg:%d\n", d->prefs.quoteIcon ? 1 : 0); appendFormat_String(str, "prefs.hoveroutline.changed arg:%d\n", d->prefs.hoverOutline); appendFormat_String(str, "theme.set arg:%d auto:1\n", d->prefs.theme); appendFormat_String(str, "ostheme arg:%d\n", d->prefs.useSystemTheme); + appendFormat_String(str, "doctheme.dark.set arg:%d\n", d->prefs.docThemeDark); + appendFormat_String(str, "doctheme.light.set arg:%d\n", d->prefs.docThemeLight); appendFormat_String(str, "saturation.set arg:%d\n", (int) ((d->prefs.saturation * 100) + 0.5f)); appendFormat_String(str, "proxy.gopher address:%s\n", cstr_String(&d->prefs.gopherProxy)); appendFormat_String(str, "proxy.http address:%s\n", cstr_String(&d->prefs.httpProxy)); @@ -253,9 +257,9 @@ static iBool loadState_App_(iApp *d) { printf("%s: format not recognized\n", cstr_String(path_File(f))); return iFalse; } - const int version = read32_File(f); + const uint32_t version = readU32_File(f); /* Check supported versions. */ - if (version != 0) { + if (version > latest_FileVersion) { printf("%s: unsupported version\n", cstr_String(path_File(f))); return iFalse; } @@ -301,7 +305,7 @@ static void saveState_App_(const iApp *d) { iFile *f = newCStr_File(concatPath_CStr(dataDir_App_, stateFileName_App_)); if (open_File(f, writeOnly_FileMode)) { writeData_File(f, magicState_App_, 4); - write32_File(f, 0); /* version */ + writeU32_File(f, latest_FileVersion); /* version */ iConstForEach(ObjectList, i, iClob(listDocuments_App())) { if (isInstance_Object(i.object, &Class_DocumentWidget)) { writeData_File(f, magicTabDocument_App_, 4); @@ -700,6 +704,18 @@ static void updatePrefsThemeButtons_(iWidget *d) { } } +static void updateColorThemeButton_(iLabelWidget *button, int theme) { + const char *mode = strstr(cstr_String(id_Widget(as_Widget(button))), ".dark") ? "dark" : "light"; + const char *command = format_CStr("doctheme.%s.set arg:%d", mode, theme); + iForEach(ObjectList, i, children_Widget(findChild_Widget(as_Widget(button), "menu"))) { + iLabelWidget *item = i.object; + if (!cmp_String(command_LabelWidget(item), command)) { + updateText_LabelWidget(button, label_LabelWidget(item)); + break; + } + } +} + static iBool handlePrefsCommands_(iWidget *d, const char *cmd) { if (equal_Command(cmd, "prefs.dismiss") || equal_Command(cmd, "preferences")) { setUiScale_Window(get_Window(), @@ -720,6 +736,20 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) { destroy_Widget(d); return iTrue; } + else if (equal_Command(cmd, "quoteicon.set")) { + const int arg = arg_Command(cmd); + setFlags_Widget(findChild_Widget(d, "prefs.quoteicon.0"), selected_WidgetFlag, arg == 0); + setFlags_Widget(findChild_Widget(d, "prefs.quoteicon.1"), selected_WidgetFlag, arg == 1); + return iFalse; + } + else if (equal_Command(cmd, "doctheme.dark.set")) { + updateColorThemeButton_(findChild_Widget(d, "prefs.doctheme.dark"), arg_Command(cmd)); + return iFalse; + } + else if (equal_Command(cmd, "doctheme.light.set")) { + updateColorThemeButton_(findChild_Widget(d, "prefs.doctheme.light"), arg_Command(cmd)); + return iFalse; + } else if (equal_Command(cmd, "prefs.ostheme.changed")) { postCommandf_App("ostheme arg:%d", arg_Command(cmd)); } @@ -908,11 +938,26 @@ iBool handleCommand_App(const char *cmd) { d->prefs.useSystemTheme = arg_Command(cmd); return iTrue; } + else if (equal_Command(cmd, "doctheme.dark.set")) { + d->prefs.docThemeDark = arg_Command(cmd); + postCommand_App("theme.changed auto:1"); + return iTrue; + } + else if (equal_Command(cmd, "doctheme.light.set")) { + d->prefs.docThemeLight = arg_Command(cmd); + postCommand_App("theme.changed auto:1"); + return iTrue; + } else if (equal_Command(cmd, "linewidth.set")) { d->prefs.lineWidth = iMax(20, arg_Command(cmd)); postCommand_App("document.layout.changed"); return iTrue; } + else if (equal_Command(cmd, "quoteicon.set")) { + d->prefs.quoteIcon = arg_Command(cmd) != 0; + postCommand_App("document.layout.changed"); + return iTrue; + } else if (equal_Command(cmd, "prefs.biglede.changed")) { d->prefs.bigFirstParagraph = arg_Command(cmd) != 0; postCommand_App("document.layout.changed"); @@ -1058,8 +1103,14 @@ iBool handleCommand_App(const char *cmd) { findChild_Widget(dlg, format_CStr("prefs.linewidth.%d", d->prefs.lineWidth)), selected_WidgetFlag, iTrue); + setFlags_Widget( + findChild_Widget(dlg, format_CStr("prefs.quoteicon.%d", d->prefs.quoteIcon)), + selected_WidgetFlag, + iTrue); setToggle_Widget(findChild_Widget(dlg, "prefs.biglede"), d->prefs.bigFirstParagraph); setToggle_Widget(findChild_Widget(dlg, "prefs.sideicon"), d->prefs.sideIcon); + updateColorThemeButton_(findChild_Widget(dlg, "prefs.doctheme.dark"), d->prefs.docThemeDark); + updateColorThemeButton_(findChild_Widget(dlg, "prefs.doctheme.light"), d->prefs.docThemeLight); setFlags_Widget( findChild_Widget( dlg, format_CStr("prefs.saturation.%d", (int) (d->prefs.saturation * 3.99f))), diff --git a/src/defs.h b/src/defs.h new file mode 100644 index 000000000..3280667e8 --- /dev/null +++ b/src/defs.h @@ -0,0 +1,35 @@ +/* Copyright 2020 Jaakko Keränen + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +#pragma once + +enum iFileVersion { + initial_FileVersion = 0, + addedResponseTimestamps_FileVersion = 1, + /* meta */ + latest_FileVersion = 1 +}; + +/* Icons */ + +#define openLock_CStr "\U0001f513" +#define closedLock_CStr "\U0001f512" diff --git a/src/gmcerts.c b/src/gmcerts.c index c358573cb..b5b7b3716 100644 --- a/src/gmcerts.c +++ b/src/gmcerts.c @@ -21,6 +21,7 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "gmcerts.h" +#include "defs.h" #include #include @@ -202,7 +203,7 @@ static void saveIdentities_GmCerts_(const iGmCerts *d) { iFile *f = new_File(collect_String(concatCStr_Path(&d->saveDir, identsFilename_GmCerts_))); if (open_File(f, writeOnly_FileMode)) { writeData_File(f, magicIdMeta_GmCerts_, 4); - writeU32_File(f, 0); /* version */ + writeU32_File(f, latest_FileVersion); /* version */ iConstForEach(PtrArray, i, &d->idents) { const iGmIdentity *ident = i.ptr; if (~ident->flags & temporary_GmIdentityFlag) { @@ -245,7 +246,12 @@ static void loadIdentities_GmCerts_(iGmCerts *d) { printf("%s: format not recognized\n", cstr_String(path_File(f))); return; } - setVersion_Stream(stream_File(f), readU32_File(f)); + const uint32_t version = readU32_File(f); + if (version > latest_FileVersion) { + printf("%s: unsupported version\n", cstr_String(path_File(f))); + return; + } + setVersion_Stream(stream_File(f), version); while (!atEnd_File(f)) { readData_File(f, sizeof(magic), magic); if (!memcmp(magic, magicIdentity_GmCerts_, sizeof(magic))) { diff --git a/src/gmdocument.c b/src/gmdocument.c index 4240b960f..b8de304f4 100644 --- a/src/gmdocument.c +++ b/src/gmdocument.c @@ -265,10 +265,10 @@ static void doLayout_GmDocument_(iGmDocument *d) { 5, 10, 5, 10, 0, 0, 0, 5 }; static const float topMargin[max_GmLineType] = { - 0.0f, 0.5f, 1.0f, 0.5f, 2.0f, 1.5f, 1.0f, 1.0f + 0.0f, 0.333f, 1.0f, 0.5f, 2.0f, 1.5f, 1.0f, 1.0f }; static const float bottomMargin[max_GmLineType] = { - 0.0f, 0.5f, 1.0f, 0.5f, 0.5f, 0.5f, 0.5f, 1.0f + 0.0f, 0.333f, 1.0f, 0.5f, 0.5f, 0.5f, 0.5f, 1.0f }; static const char *arrow = "\u27a4"; static const char *envelope = "\U0001f4e7"; @@ -290,7 +290,7 @@ static void doLayout_GmDocument_(iGmDocument *d) { iRangecc contentLine = iNullRange; iInt2 pos = zero_I2(); iBool isFirstText = isGemini && prefs->bigFirstParagraph; - iBool addQuoteIcon = iTrue; + iBool addQuoteIcon = prefs->quoteIcon; iBool isPreformat = iFalse; iRangecc preAltText = iNullRange; int preFont = preformatted_FontId; @@ -446,7 +446,7 @@ static void doLayout_GmDocument_(iGmDocument *d) { pushBack_Array(&d->layout, "eRun); } else if (type != quote_GmLineType) { - addQuoteIcon = iTrue; + addQuoteIcon = prefs->quoteIcon; } /* Link icon. */ if (type == link_GmLineType) { @@ -485,6 +485,9 @@ static void doLayout_GmDocument_(iGmDocument *d) { iRangecc runLine = line; /* Create one or more text runs for this line. */ run.flags |= startOfLine_GmRunFlag; + if (!prefs->quoteIcon && type == quote_GmLineType) { + run.flags |= quoteBorder_GmRunFlag; + } iAssert(!isEmpty_Range(&runLine)); /* must have something at this point */ while (!isEmpty_Range(&runLine)) { /* Little bit of breathing space between wrapped lines. */ @@ -631,12 +634,45 @@ void reset_GmDocument(iGmDocument *d) { d->themeSeed = 0; } +static void setDerivedThemeColors_(enum iGmDocumentTheme theme) { + set_Color(tmQuoteIcon_ColorId, + mix_Color(get_Color(tmQuote_ColorId), get_Color(tmBackground_ColorId), 0.55f)); + set_Color(tmBannerSideTitle_ColorId, + mix_Color(get_Color(tmBannerTitle_ColorId), get_Color(tmBackground_ColorId), + theme == colorfulDark_GmDocumentTheme ? 0.55f : 0)); + set_Color(tmOutlineHeadingAbove_ColorId, get_Color(white_ColorId)); + set_Color(tmOutlineHeadingBelow_ColorId, get_Color(black_ColorId)); + switch (theme) { + case colorfulDark_GmDocumentTheme: + set_Color(tmOutlineHeadingBelow_ColorId, get_Color(tmBannerTitle_ColorId)); + if (equal_Color(get_Color(tmOutlineHeadingAbove_ColorId), + get_Color(tmOutlineHeadingBelow_ColorId))) { + set_Color(tmOutlineHeadingBelow_ColorId, get_Color(tmHeading3_ColorId)); + } + break; + case colorfulLight_GmDocumentTheme: + case sepia_GmDocumentTheme: + set_Color(tmOutlineHeadingAbove_ColorId, get_Color(black_ColorId)); + set_Color(tmOutlineHeadingBelow_ColorId, mix_Color(get_Color(tmBackground_ColorId), get_Color(black_ColorId), 0.6f)); + break; + case gray_GmDocumentTheme: + set_Color(tmOutlineHeadingBelow_ColorId, get_Color(gray75_ColorId)); + break; + case white_GmDocumentTheme: + set_Color(tmOutlineHeadingBelow_ColorId, mix_Color(get_Color(tmBannerIcon_ColorId), get_Color(white_ColorId), 0.6f)); + break; + case highContrast_GmDocumentTheme: + set_Color(tmOutlineHeadingAbove_ColorId, get_Color(black_ColorId)); + break; + default: + break; + } +} + void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) { - const iPrefs * prefs = prefs_App(); + const iPrefs * prefs = prefs_App(); enum iGmDocumentTheme theme = (isDark_ColorTheme(colorTheme_App()) ? prefs->docThemeDark : prefs->docThemeLight); -// const iBool isLightMode = isLight_ColorTheme(colorTheme_App()); -// const iBool isDarkMode = !isLightMode; static const iChar siteIcons[] = { 0x203b, 0x2042, 0x205c, 0x2182, 0x25ed, 0x2600, 0x2601, 0x2604, 0x2605, 0x2606, 0x265c, 0x265e, 0x2690, 0x2691, 0x2693, 0x2698, 0x2699, 0x26f0, 0x270e, 0x2728, @@ -645,20 +681,10 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) { 0x1f306, 0x1f308, 0x1f30a, 0x1f319, 0x1f31f, 0x1f320, 0x1f340, 0x1f4cd, 0x1f4e1, 0x1f531, 0x1f533, 0x1f657, 0x1f659, 0x1f665, 0x1f668, 0x1f66b, 0x1f78b, 0x1f796, 0x1f79c, }; - /* Default colors. */ { - if (theme == colorfulDark_GmDocumentTheme) { - const iHSLColor base = { 200, 0, 0.15f, 1.0f }; - setHsl_Color(tmBackground_ColorId, base); - set_Color(tmParagraph_ColorId, get_Color(gray75_ColorId)); - setHsl_Color(tmFirstParagraph_ColorId, addSatLum_HSLColor(base, 0, 0.75f)); - set_Color(tmQuote_ColorId, get_Color(cyan_ColorId)); - set_Color(tmPreformatted_ColorId, get_Color(cyan_ColorId)); - set_Color(tmHeading1_ColorId, get_Color(white_ColorId)); - setHsl_Color(tmHeading2_ColorId, addSatLum_HSLColor(base, 0.5f, 0.5f)); - setHsl_Color(tmHeading3_ColorId, addSatLum_HSLColor(base, 1.0f, 0.4f)); - set_Color(tmBannerBackground_ColorId, get_Color(black_ColorId)); - set_Color(tmBannerTitle_ColorId, get_Color(white_ColorId)); - set_Color(tmBannerIcon_ColorId, get_Color(orange_ColorId)); + /* Default colors. These are used on "about:" pages and local files, for example. */ { + /* Link colors are generally the same in all themes. */ + set_Color(tmBadLink_ColorId, get_Color(red_ColorId)); + if (isDark_GmDocumentTheme(theme)) { set_Color(tmInlineContentMetadata_ColorId, get_Color(cyan_ColorId)); set_Color(tmLinkText_ColorId, get_Color(white_ColorId)); set_Color(tmLinkIcon_ColorId, get_Color(cyan_ColorId)); @@ -680,18 +706,6 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) { set_Color(tmGopherLinkLastVisitDate_ColorId, get_Color(blue_ColorId)); } else { - const iHSLColor base = { 40, 0, 1.0f, 1.0f }; - setHsl_Color(tmBackground_ColorId, base); - set_Color(tmParagraph_ColorId, get_Color(gray25_ColorId)); - set_Color(tmFirstParagraph_ColorId, get_Color(black_ColorId)); - set_Color(tmQuote_ColorId, get_Color(brown_ColorId)); - set_Color(tmPreformatted_ColorId, get_Color(brown_ColorId)); - set_Color(tmHeading1_ColorId, get_Color(black_ColorId)); - setHsl_Color(tmHeading2_ColorId, addSatLum_HSLColor(base, 0.15f, -0.7f)); - setHsl_Color(tmHeading3_ColorId, addSatLum_HSLColor(base, 0.3f, -0.6f)); - set_Color(tmBannerBackground_ColorId, get_Color(white_ColorId)); - set_Color(tmBannerTitle_ColorId, get_Color(gray50_ColorId)); - set_Color(tmBannerIcon_ColorId, get_Color(teal_ColorId)); set_Color(tmInlineContentMetadata_ColorId, get_Color(brown_ColorId)); set_Color(tmLinkText_ColorId, get_Color(black_ColorId)); set_Color(tmLinkIcon_ColorId, get_Color(teal_ColorId)); @@ -712,7 +726,111 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) { set_Color(tmGopherLinkDomain_ColorId, get_Color(magenta_ColorId)); set_Color(tmGopherLinkLastVisitDate_ColorId, get_Color(blue_ColorId)); } - set_Color(tmBadLink_ColorId, get_Color(red_ColorId)); + /* Set the non-link default colors. Note that some/most of these are overwritten later + if a theme seed if available. */ + if (theme == colorfulDark_GmDocumentTheme) { + const iHSLColor base = { 200, 0, 0.15f, 1.0f }; + setHsl_Color(tmBackground_ColorId, base); + set_Color(tmParagraph_ColorId, get_Color(gray75_ColorId)); + setHsl_Color(tmFirstParagraph_ColorId, addSatLum_HSLColor(base, 0, 0.75f)); + set_Color(tmQuote_ColorId, get_Color(cyan_ColorId)); + set_Color(tmPreformatted_ColorId, get_Color(cyan_ColorId)); + set_Color(tmHeading1_ColorId, get_Color(white_ColorId)); + setHsl_Color(tmHeading2_ColorId, addSatLum_HSLColor(base, 0.5f, 0.5f)); + setHsl_Color(tmHeading3_ColorId, addSatLum_HSLColor(base, 1.0f, 0.4f)); + setHsl_Color(tmBannerBackground_ColorId, addSatLum_HSLColor(base, 0, -0.05f)); + set_Color(tmBannerTitle_ColorId, get_Color(white_ColorId)); + set_Color(tmBannerIcon_ColorId, get_Color(orange_ColorId)); + } + else if (theme == colorfulLight_GmDocumentTheme) { + const iHSLColor base = addSatLum_HSLColor(get_HSLColor(teal_ColorId), -0.3f, 0.5f); + setHsl_Color(tmBackground_ColorId, base); + set_Color(tmParagraph_ColorId, get_Color(black_ColorId)); + set_Color(tmFirstParagraph_ColorId, get_Color(black_ColorId)); + setHsl_Color(tmQuote_ColorId, addSatLum_HSLColor(base, 0, -0.25f)); + setHsl_Color(tmPreformatted_ColorId, addSatLum_HSLColor(base, 0, -0.3f)); + set_Color(tmHeading1_ColorId, get_Color(white_ColorId)); + set_Color(tmHeading2_ColorId, mix_Color(get_Color(tmBackground_ColorId), get_Color(black_ColorId), 0.67f)); + set_Color(tmHeading3_ColorId, mix_Color(get_Color(tmBackground_ColorId), get_Color(black_ColorId), 0.55f)); + setHsl_Color(tmBannerBackground_ColorId, addSatLum_HSLColor(base, 0, -0.1f)); + setHsl_Color(tmBannerIcon_ColorId, addSatLum_HSLColor(base, 0, -0.2f)); + setHsl_Color(tmBannerTitle_ColorId, addSatLum_HSLColor(base, 0, -0.2f)); + setHsl_Color(tmLinkIcon_ColorId, addSatLum_HSLColor(get_HSLColor(teal_ColorId), 0, 0)); + set_Color(tmLinkIconVisited_ColorId, mix_Color(get_Color(tmBackground_ColorId), get_Color(teal_ColorId), 0.35f)); + set_Color(tmLinkDomain_ColorId, get_Color(teal_ColorId)); + setHsl_Color(tmHypertextLinkIcon_ColorId, get_HSLColor(white_ColorId)); + set_Color(tmHypertextLinkIconVisited_ColorId, mix_Color(get_Color(tmBackground_ColorId), get_Color(white_ColorId), 0.5f)); + set_Color(tmHypertextLinkDomain_ColorId, get_Color(brown_ColorId)); + setHsl_Color(tmGopherLinkIcon_ColorId, addSatLum_HSLColor(get_HSLColor(tmGopherLinkIcon_ColorId), 0, -0.25f)); + setHsl_Color(tmGopherLinkTextHover_ColorId, addSatLum_HSLColor(get_HSLColor(tmGopherLinkTextHover_ColorId), 0, -0.3f)); + } + else if (theme == black_GmDocumentTheme) { + set_Color(tmBackground_ColorId, get_Color(black_ColorId)); + set_Color(tmParagraph_ColorId, get_Color(gray75_ColorId)); + set_Color(tmFirstParagraph_ColorId, mix_Color(get_Color(gray75_ColorId), get_Color(white_ColorId), 0.5f)); + set_Color(tmQuote_ColorId, get_Color(orange_ColorId)); + set_Color(tmPreformatted_ColorId, get_Color(orange_ColorId)); + set_Color(tmHeading1_ColorId, get_Color(cyan_ColorId)); + set_Color(tmHeading2_ColorId, mix_Color(get_Color(cyan_ColorId), get_Color(white_ColorId), 0.66f)); + set_Color(tmHeading3_ColorId, get_Color(white_ColorId)); + set_Color(tmBannerBackground_ColorId, get_Color(black_ColorId)); + set_Color(tmBannerTitle_ColorId, get_Color(teal_ColorId)); + set_Color(tmBannerIcon_ColorId, get_Color(teal_ColorId)); + } + else if (theme == gray_GmDocumentTheme) { + set_Color(tmBackground_ColorId, mix_Color(get_Color(gray25_ColorId), get_Color(black_ColorId), 0.25f)); + set_Color(tmParagraph_ColorId, mix_Color(get_Color(gray75_ColorId), get_Color(white_ColorId), 0.25f)); + set_Color(tmFirstParagraph_ColorId, mix_Color(get_Color(gray75_ColorId), get_Color(white_ColorId), 0.5f)); + set_Color(tmQuote_ColorId, get_Color(orange_ColorId)); + set_Color(tmPreformatted_ColorId, get_Color(orange_ColorId)); + set_Color(tmHeading1_ColorId, get_Color(cyan_ColorId)); + set_Color(tmHeading2_ColorId, mix_Color(get_Color(cyan_ColorId), get_Color(white_ColorId), 0.66f)); + set_Color(tmHeading3_ColorId, get_Color(white_ColorId)); + set_Color(tmBannerBackground_ColorId, mix_Color(get_Color(gray25_ColorId), get_Color(black_ColorId), 0.5f)); + set_Color(tmBannerTitle_ColorId, get_Color(teal_ColorId)); + set_Color(tmBannerIcon_ColorId, get_Color(teal_ColorId)); + } + else if (theme == sepia_GmDocumentTheme) { + const iHSLColor base = { 40, 0.6f, 0.9f, 1.0f }; + setHsl_Color(tmBackground_ColorId, base); + set_Color(tmParagraph_ColorId, get_Color(black_ColorId)); + set_Color(tmFirstParagraph_ColorId, get_Color(black_ColorId)); + set_Color(tmQuote_ColorId, get_Color(brown_ColorId)); + set_Color(tmPreformatted_ColorId, get_Color(brown_ColorId)); + set_Color(tmHeading1_ColorId, get_Color(brown_ColorId)); + set_Color(tmHeading2_ColorId, mix_Color(get_Color(brown_ColorId), get_Color(black_ColorId), 0.5f)); + set_Color(tmHeading3_ColorId, get_Color(black_ColorId)); + set_Color(tmBannerBackground_ColorId, mix_Color(get_Color(tmBackground_ColorId), get_Color(brown_ColorId), 0.15f)); + set_Color(tmBannerTitle_ColorId, get_Color(brown_ColorId)); + set_Color(tmBannerIcon_ColorId, get_Color(brown_ColorId)); + } + else if (theme == white_GmDocumentTheme) { + const iHSLColor base = { 40, 0, 1.0f, 1.0f }; + setHsl_Color(tmBackground_ColorId, base); + set_Color(tmParagraph_ColorId, get_Color(gray25_ColorId)); + set_Color(tmFirstParagraph_ColorId, get_Color(black_ColorId)); + set_Color(tmQuote_ColorId, get_Color(brown_ColorId)); + set_Color(tmPreformatted_ColorId, get_Color(brown_ColorId)); + set_Color(tmHeading1_ColorId, get_Color(black_ColorId)); + setHsl_Color(tmHeading2_ColorId, addSatLum_HSLColor(base, 0.15f, -0.7f)); + setHsl_Color(tmHeading3_ColorId, addSatLum_HSLColor(base, 0.3f, -0.6f)); + set_Color(tmBannerBackground_ColorId, get_Color(white_ColorId)); + set_Color(tmBannerTitle_ColorId, get_Color(gray50_ColorId)); + set_Color(tmBannerIcon_ColorId, get_Color(teal_ColorId)); + } + else if (theme == highContrast_GmDocumentTheme) { + set_Color(tmBackground_ColorId, get_Color(white_ColorId)); + set_Color(tmParagraph_ColorId, get_Color(black_ColorId)); + set_Color(tmFirstParagraph_ColorId, get_Color(black_ColorId)); + set_Color(tmQuote_ColorId, get_Color(black_ColorId)); + set_Color(tmPreformatted_ColorId, get_Color(black_ColorId)); + set_Color(tmHeading1_ColorId, get_Color(black_ColorId)); + set_Color(tmHeading2_ColorId, get_Color(black_ColorId)); + set_Color(tmHeading3_ColorId, get_Color(black_ColorId)); + set_Color(tmBannerBackground_ColorId, mix_Color(get_Color(gray75_ColorId), get_Color(white_ColorId), 0.75f)); + set_Color(tmBannerTitle_ColorId, get_Color(black_ColorId)); + set_Color(tmBannerIcon_ColorId, get_Color(black_ColorId)); + } /* Apply the saturation setting. */ for (int i = tmFirst_ColorId; i < max_ColorId; i++) { if (!isLink_ColorId(i)) { @@ -773,8 +891,9 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) { const iBool isDarkBgSat = (d->themeSeed & 0x200000) != 0 && (primIndex < 1 || primIndex > 4); - // printf("background: %d %f %f\n", (int) base.hue, base.sat, base.lum); - // printf("isDarkBgSat: %d\n", isDarkBgSat); + static const float normLum[] = { 0.8f, 0.7f, 0.675f, 0.65f, 0.55f, + 0.6f, 0.475f, 0.475f, 0.75f, 0.8f, + 0.85f, 0.85f }; if (theme == colorfulDark_GmDocumentTheme) { iHSLColor base = { hues[primIndex], @@ -819,7 +938,32 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) { set_Color(tmQuote_ColorId, get_Color(tmPreformatted_ColorId)); set_Color(tmInlineContentMetadata_ColorId, get_Color(tmHeading3_ColorId)); } - else { + else if (theme == colorfulLight_GmDocumentTheme) { +// static int primIndex = 0; +// primIndex = (primIndex + 1) % iElemCount(hues); + iHSLColor base = { hues[primIndex], 1.0f, normLum[primIndex], 1.0f }; +// printf("prim:%d norm:%f\n", primIndex, normLum[primIndex]); fflush(stdout); + static const float normSat[] = { + 0.85f, 0.9f, 1, 0.65f, 0.65f, + 0.65f, 0.9f, 0.9f, 1, 0.9f, + 1, 0.75f + }; + iBool darkHeadings = iTrue; + base.sat *= normSat[primIndex] * 0.8f; + setHsl_Color(tmBackground_ColorId, base); + set_Color(tmParagraph_ColorId, get_Color(black_ColorId)); + set_Color(tmFirstParagraph_ColorId, get_Color(black_ColorId)); + setHsl_Color(tmQuote_ColorId, addSatLum_HSLColor(base, 0, -base.lum * 0.67f)); + setHsl_Color(tmPreformatted_ColorId, addSatLum_HSLColor(base, 0, -base.lum * 0.75f)); + set_Color(tmHeading1_ColorId, get_Color(white_ColorId)); + set_Color(tmHeading2_ColorId, mix_Color(get_Color(tmBackground_ColorId), get_Color(darkHeadings ? black_ColorId : white_ColorId), 0.7f)); + set_Color(tmHeading3_ColorId, mix_Color(get_Color(tmBackground_ColorId), get_Color(darkHeadings ? black_ColorId : white_ColorId), 0.6f)); + setHsl_Color(tmBannerBackground_ColorId, addSatLum_HSLColor(base, 0, -0.04f)); + setHsl_Color(tmBannerIcon_ColorId, addSatLum_HSLColor(base, 0, -0.3f)); + setHsl_Color(tmBannerTitle_ColorId, addSatLum_HSLColor(base, 0, -0.25f)); + set_Color(tmLinkIconVisited_ColorId, mix_Color(get_Color(tmBackground_ColorId), get_Color(teal_ColorId), 0.3f)); + } + else if (theme == white_GmDocumentTheme) { iHSLColor base = { hues[primIndex], 1.0f, 0.3f, 1.0f }; iHSLColor altBase = { altHue, base.sat, base.lum - 0.1f, 1 }; @@ -828,7 +972,7 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) { setHsl_Color(tmBannerTitle_ColorId, addSatLum_HSLColor(base, -0.6f, 0.25f)); setHsl_Color(tmBannerIcon_ColorId, addSatLum_HSLColor(base, 0, 0)); - setHsl_Color(tmHeading1_ColorId, base); //addSatLum_HSLColor(base, -0.5f, 0.125f)); + setHsl_Color(tmHeading1_ColorId, base); set_Color(tmHeading2_ColorId, mix_Color(rgb_HSLColor(base), rgb_HSLColor(altBase), 0.5f)); setHsl_Color(tmHeading3_ColorId, altBase); @@ -838,39 +982,23 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) { set_Color(tmQuote_ColorId, get_Color(tmPreformatted_ColorId)); set_Color(tmInlineContentMetadata_ColorId, get_Color(tmHeading3_ColorId)); } + else if (theme == black_GmDocumentTheme || theme == gray_GmDocumentTheme) { + const float primHue = hues[primIndex]; + const iHSLColor primBright = { primHue, 1, 0.6f, 1 }; + const iHSLColor primDim = { primHue, 1, normLum[primIndex] + (theme == gray_GmDocumentTheme ? 0.0f : -0.3f), 1}; + const iHSLColor altBright = { altHue, 1, normLum[altIndex[0]] + (theme == gray_GmDocumentTheme ? 0.1f : 0.0f), 1 }; + setHsl_Color(tmQuote_ColorId, altBright); + setHsl_Color(tmPreformatted_ColorId, altBright); + setHsl_Color(tmHeading1_ColorId, primBright); + set_Color(tmHeading2_ColorId, mix_Color(get_Color(tmHeading1_ColorId), get_Color(white_ColorId), 0.66f)); + setHsl_Color(tmBannerTitle_ColorId, primDim); + setHsl_Color(tmBannerIcon_ColorId, primDim); + } /* Adjust colors based on light/dark mode. */ for (int i = tmFirst_ColorId; i < max_ColorId; i++) { iHSLColor color = hsl_Color(get_Color(i)); - if (theme == white_GmDocumentTheme) { -#if 0 - if (isLink_ColorId(i)) continue; - color.lum = 1.0f - color.lum; /* All colors invert lightness. */ - if (isRegularText_ColorId(i)) { - /* Darken paragraphs and default state link text. */ - color.lum *= 0.5f; - } - else if (i == tmBackground_ColorId) { - color.sat = (color.sat + 1) / 2; - color.lum += 0.06f; - } - else if (i == tmHeading3_ColorId) { - color.lum *= 0.75f; - } - else if (i == tmBannerIcon_ColorId || i == tmBannerTitle_ColorId) { - color.sat = 1.0f; - color.lum = 0.35f; - } - else if (i == tmBannerBackground_ColorId) { - color = hsl_Color(get_Color(tmBackground_ColorId)); - } - else if (isText_ColorId(i)) { - color.sat = 0.9f; - color.lum = (9 * color.lum + 0.5f) / 10; - } -#endif - } - else { /* dark mode */ + if (theme == colorfulDark_GmDocumentTheme) { /* dark mode */ if (!isLink_ColorId(i)) { if (isDarkBgSat) { /* Saturate background, desaturate text. */ @@ -907,17 +1035,20 @@ void setThemeSeed_GmDocument(iGmDocument *d, const iBlock *seed) { } } /* Derived colors. */ - set_Color(tmQuoteIcon_ColorId, - mix_Color(get_Color(tmQuote_ColorId), get_Color(tmBackground_ColorId), 0.55f)); - set_Color(tmBannerSideTitle_ColorId, - mix_Color(get_Color(tmBannerTitle_ColorId), get_Color(tmBackground_ColorId), - theme == colorfulDark_GmDocumentTheme ? 0.55f : 0)); + setDerivedThemeColors_(theme); /* Special exceptions. */ if (seed) { if (equal_CStr(cstr_Block(seed), "gemini.circumlunar.space")) { d->siteIcon = 0x264a; /* gemini symbol */ } } +#if 0 + for (int i = tmFirst_ColorId; i < max_ColorId; ++i) { + const iColor tc = get_Color(i); + printf("%02i: #%02x%02x%02x\n", i, tc.r, tc.g, tc.b); + } + printf("---\n"); +#endif } void setFormat_GmDocument(iGmDocument *d, enum iGmDocumentFormat format) { diff --git a/src/gmdocument.h b/src/gmdocument.h index 19a6036f3..f27446ab4 100644 --- a/src/gmdocument.h +++ b/src/gmdocument.h @@ -36,9 +36,19 @@ iDeclareType(GmRun) enum iGmDocumentTheme { colorfulDark_GmDocumentTheme, + colorfulLight_GmDocumentTheme, + black_GmDocumentTheme, + gray_GmDocumentTheme, white_GmDocumentTheme, + sepia_GmDocumentTheme, + highContrast_GmDocumentTheme, }; +iLocalDef iBool isDark_GmDocumentTheme(enum iGmDocumentTheme d) { + return d == colorfulDark_GmDocumentTheme || d == black_GmDocumentTheme || + d == gray_GmDocumentTheme; +} + typedef uint16_t iGmLinkId; enum iGmLinkFlags { @@ -69,6 +79,7 @@ enum iGmRunFlags { startOfLine_GmRunFlag = iBit(2), endOfLine_GmRunFlag = iBit(3), siteBanner_GmRunFlag = iBit(4), /* area reserved for the site banner */ + quoteBorder_GmRunFlag = iBit(5), }; struct Impl_GmRun { diff --git a/src/gmrequest.c b/src/gmrequest.c index 7b6414d2e..3faa28335 100644 --- a/src/gmrequest.c +++ b/src/gmrequest.c @@ -26,6 +26,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "app.h" /* dataDir_App() */ #include "embedded.h" #include "ui/text.h" +#include "defs.h" #include #include @@ -86,7 +87,7 @@ void serialize_GmResponse(const iGmResponse *d, iStream *outs) { write32_Stream(outs, d->certFlags); serialize_Date(&d->certValidUntil, outs); serialize_String(&d->certSubject, outs); - /* TODO: Include the timestamp. */ + writeU64_Stream(outs, d->when.ts.tv_sec); } void deserialize_GmResponse(iGmResponse *d, iStream *ins) { @@ -96,6 +97,10 @@ void deserialize_GmResponse(iGmResponse *d, iStream *ins) { d->certFlags = read32_Stream(ins); deserialize_Date(&d->certValidUntil, ins); deserialize_String(&d->certSubject, ins); + iZap(d->when); + if (version_Stream(ins) >= addedResponseTimestamps_FileVersion) { + d->when.ts.tv_sec = readU64_Stream(ins); + } } /*----------------------------------------------------------------------------------------------*/ diff --git a/src/gmutil.c b/src/gmutil.c index f55729d10..43586e55e 100644 --- a/src/gmutil.c +++ b/src/gmutil.c @@ -214,7 +214,7 @@ static const struct { "The requested resource does not exist." } }, { unsupportedMimeType_GmStatusCode, { 0x1f47d, /* alien */ - "Unsupported MIME Type", + "Unsupported Content Type", "The received content cannot be viewed with this application." } }, { invalidHeader_GmStatusCode, { 0x1f4a9, /* pile of poo */ diff --git a/src/prefs.c b/src/prefs.c index 146f38388..1fcb1b8ea 100644 --- a/src/prefs.c +++ b/src/prefs.c @@ -7,6 +7,7 @@ void init_Prefs(iPrefs *d) { d->retainWindowSize = iTrue; d->zoomPercent = 100; d->forceLineWrap = iFalse; + d->quoteIcon = iTrue; d->font = nunito_TextFont; d->headingFont = nunito_TextFont; d->lineWidth = 40; diff --git a/src/prefs.h b/src/prefs.h index a19cc0ca4..bfc0c1742 100644 --- a/src/prefs.h +++ b/src/prefs.h @@ -25,6 +25,7 @@ struct Impl_Prefs { int lineWidth; iBool bigFirstParagraph; iBool forceLineWrap; + iBool quoteIcon; iBool sideIcon; iBool hoverOutline; enum iGmDocumentTheme docThemeDark; diff --git a/src/ui/color.h b/src/ui/color.h index d1c526854..2b7fd735f 100644 --- a/src/ui/color.h +++ b/src/ui/color.h @@ -118,6 +118,8 @@ enum iColorId { tmBannerTitle_ColorId, tmBannerIcon_ColorId, tmBannerSideTitle_ColorId, + tmOutlineHeadingAbove_ColorId, + tmOutlineHeadingBelow_ColorId, tmInlineContentMetadata_ColorId, tmBadLink_ColorId, diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 85c17a5b5..9e8e644ea 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -25,10 +25,12 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "app.h" #include "audio/player.h" #include "command.h" +#include "defs.h" #include "gmdocument.h" #include "gmrequest.h" #include "gmutil.h" #include "history.h" +#include "indicatorwidget.h" #include "inputwidget.h" #include "keys.h" #include "labelwidget.h" @@ -137,15 +139,13 @@ struct Impl_OutlineItem { iRangecc text; int font; iRect rect; - int seenColor; /* TODO: not used */ - int sepColor; }; /*----------------------------------------------------------------------------------------------*/ -static void animatePlayingAudio_DocumentWidget_(void *); +static void animatePlayers_DocumentWidget_(iDocumentWidget *d); -static const int smoothSpeed_DocumentWidget_ = 120; /* unit: gap_Text per second */ +static const int smoothDuration_DocumentWidget_ = 600; /* milliseconds */ static const int outlineMinWidth_DocumentWdiget_ = 45; /* times gap_UI */ static const int outlineMaxWidth_DocumentWidget_ = 65; /* times gap_UI */ static const int outlinePadding_DocumentWidget_ = 3; /* times gap_UI */ @@ -157,10 +157,17 @@ enum iRequestState { ready_RequestState, }; +enum iDocumentWidgetFlag { + selecting_DocumentWidgetFlag = iBit(1), + noHoverWhileScrolling_DocumentWidgetFlag = iBit(2), + showLinkNumbers_DocumentWidgetFlag = iBit(3), +}; + struct Impl_DocumentWidget { iWidget widget; enum iRequestState state; iModel mod; + int flags; iString * titleUser; iGmRequest * request; iAtomicInt isRequestUpdated; /* request has new content, need to parse it */ @@ -173,7 +180,6 @@ struct Impl_DocumentWidget { iDate certExpiry; iString * certSubject; int redirectCount; - iBool selecting; iRangecc selectMark; iRangecc foundMark; int pageMargin; @@ -181,23 +187,18 @@ struct Impl_DocumentWidget { iPtrArray visiblePlayers; /* currently playing audio */ const iGmRun * grabbedPlayer; /* currently adjusting volume in a player */ float grabbedStartVolume; + int playerTimer; const iGmRun * hoverLink; const iGmRun * contextLink; - iBool noHoverWhileScrolling; - iBool showLinkNumbers; const iGmRun * firstVisibleRun; const iGmRun * lastVisibleRun; iClick click; float initNormScrollY; - int scrollY; - iScrollWidget *scroll; - int smoothScroll; - int smoothSpeed; - int smoothLastOffset; - iBool smoothContinue; + iAnim scrollY; iAnim sideOpacity; iAnim outlineOpacity; iArray outline; + iScrollWidget *scroll; iWidget * menu; iWidget * playerMenu; iVisBuf * visBuf; @@ -212,6 +213,7 @@ void init_DocumentWidget(iDocumentWidget *d) { setId_Widget(w, "document000"); setFlags_Widget(w, hover_WidgetFlag, iTrue); init_Model(&d->mod); + d->flags = 0; iZap(d->certExpiry); d->certFlags = 0; d->certSubject = new_String(); @@ -223,19 +225,12 @@ void init_DocumentWidget(iDocumentWidget *d) { d->doc = new_GmDocument(); d->redirectCount = 0; d->initNormScrollY = 0; - d->scrollY = 0; - d->smoothScroll = 0; - d->smoothSpeed = 0; - d->smoothLastOffset = 0; - d->smoothContinue = iFalse; - d->selecting = iFalse; + init_Anim(&d->scrollY, 0); d->selectMark = iNullRange; d->foundMark = iNullRange; d->pageMargin = 5; d->hoverLink = NULL; d->contextLink = NULL; - d->noHoverWhileScrolling = iFalse; - d->showLinkNumbers = iFalse; d->firstVisibleRun = NULL; d->lastVisibleRun = NULL; d->visBuf = new_VisBuf(); @@ -248,10 +243,14 @@ void init_DocumentWidget(iDocumentWidget *d) { init_PtrArray(&d->visibleLinks); init_PtrArray(&d->visiblePlayers); d->grabbedPlayer = NULL; + d->playerTimer = 0; init_Click(&d->click, d, SDL_BUTTON_LEFT); addChild_Widget(w, iClob(d->scroll = new_ScrollWidget())); d->menu = NULL; /* created when clicking */ d->playerMenu = NULL; + addChildFlags_Widget(w, + iClob(new_IndicatorWidget()), + resizeToParentWidth_WidgetFlag | resizeToParentHeight_WidgetFlag); #if !defined (iPlatformApple) /* in system menu */ addAction_Widget(w, reload_KeyShortcut, "navigate.reload"); addAction_Widget(w, SDLK_w, KMOD_PRIMARY, "tabs.close"); @@ -261,7 +260,6 @@ void init_DocumentWidget(iDocumentWidget *d) { } void deinit_DocumentWidget(iDocumentWidget *d) { - removeTicker_App(animatePlayingAudio_DocumentWidget_, d); delete_VisBuf(d->visBuf); delete_PtrSet(d->invalidRuns); deinit_Array(&d->outline); @@ -270,6 +268,9 @@ void deinit_DocumentWidget(iDocumentWidget *d) { deinit_Block(&d->sourceContent); deinit_String(&d->sourceMime); iRelease(d->doc); + if (d->playerTimer) { + SDL_RemoveTimer(d->playerTimer); + } deinit_PtrArray(&d->visiblePlayers); deinit_PtrArray(&d->visibleLinks); delete_String(d->certSubject); @@ -295,13 +296,6 @@ static void requestFinished_DocumentWidget_(iAnyObject *obj) { postCommand_Widget(obj, "document.request.finished doc:%p request:%p", d, d->request); } -static void resetSmoothScroll_DocumentWidget_(iDocumentWidget *d) { - d->smoothSpeed = 0; - d->smoothScroll = 0; - d->smoothLastOffset = 0; - d->smoothContinue = iFalse; -} - static int documentWidth_DocumentWidget_(const iDocumentWidget *d) { const iWidget *w = constAs_Widget(d); const iRect bounds = bounds_Widget(w); @@ -342,13 +336,15 @@ static int forceBreakWidth_DocumentWidget_(const iDocumentWidget *d) { } static iInt2 documentPos_DocumentWidget_(const iDocumentWidget *d, iInt2 pos) { - return addY_I2(sub_I2(pos, topLeft_Rect(documentBounds_DocumentWidget_(d))), d->scrollY); + return addY_I2(sub_I2(pos, topLeft_Rect(documentBounds_DocumentWidget_(d))), + value_Anim(&d->scrollY)); } static iRangei visibleRange_DocumentWidget_(const iDocumentWidget *d) { const int margin = !hasSiteBanner_GmDocument(d->doc) ? gap_UI * d->pageMargin : 0; - return (iRangei){ d->scrollY - margin, - d->scrollY + height_Rect(bounds_Widget(constAs_Widget(d))) - margin }; + return (iRangei){ value_Anim(&d->scrollY) - margin, + value_Anim(&d->scrollY) + height_Rect(bounds_Widget(constAs_Widget(d))) - + margin }; } static void addVisible_DocumentWidget_(void *context, const iGmRun *run) { @@ -370,7 +366,7 @@ static void addVisible_DocumentWidget_(void *context, const iGmRun *run) { static float normScrollPos_DocumentWidget_(const iDocumentWidget *d) { const int docSize = size_GmDocument(d->doc).y; if (docSize) { - return (float) d->scrollY / (float) docSize; + return value_Anim(&d->scrollY) / (float) docSize; } return 0; } @@ -395,8 +391,8 @@ static void updateHover_DocumentWidget_(iDocumentWidget *d, iInt2 mouse) { const iRect docBounds = documentBounds_DocumentWidget_(d); const iGmRun * oldHoverLink = d->hoverLink; d->hoverLink = NULL; - const iInt2 hoverPos = addY_I2(sub_I2(mouse, topLeft_Rect(docBounds)), d->scrollY); - if (isHover_Widget(w) && !d->noHoverWhileScrolling && + const iInt2 hoverPos = addY_I2(sub_I2(mouse, topLeft_Rect(docBounds)), value_Anim(&d->scrollY)); + if (isHover_Widget(w) && (~d->flags & noHoverWhileScrolling_DocumentWidgetFlag) && (d->state == ready_RequestState || d->state == receivedPartialResponse_RequestState)) { iConstForEach(PtrArray, i, &d->visibleLinks) { const iGmRun *run = i.ptr; @@ -435,7 +431,7 @@ static void animate_DocumentWidget_(void *ticker) { static void updateSideOpacity_DocumentWidget_(iDocumentWidget *d, iBool isAnimated) { float opacity = 0.0f; const iGmRun *banner = siteBanner_GmDocument(d->doc); - if (banner && bottom_Rect(banner->visBounds) < d->scrollY) { + if (banner && bottom_Rect(banner->visBounds) < value_Anim(&d->scrollY)) { opacity = 1.0f; } setValue_Anim(&d->sideOpacity, opacity, isAnimated ? (opacity < 0.5f ? 100 : 200) : 0); @@ -455,22 +451,59 @@ static void updateOutlineOpacity_DocumentWidget_(iDocumentWidget *d) { animate_DocumentWidget_(d); } -static void animatePlayingAudio_DocumentWidget_(void *widget) { - iDocumentWidget *d = widget; - if (document_App() != d) return; +static uint32_t playerUpdateInterval_DocumentWidget_(const iDocumentWidget *d) { + if (document_App() != d) { + return 0; + } + uint32_t interval = 0; iConstForEach(PtrArray, i, &d->visiblePlayers) { const iGmRun *run = i.ptr; iPlayer * plr = audioPlayer_Media(media_GmDocument(d->doc), run->audioId); - if (idleTimeMs_Player(plr) > 3000 && ~flags_Player(plr) & volumeGrabbed_PlayerFlag && - flags_Player(plr) & adjustingVolume_PlayerFlag) { - setFlags_Player(plr, adjustingVolume_PlayerFlag, iFalse); - refresh_Widget(d); + if (flags_Player(plr) & adjustingVolume_PlayerFlag || + (isStarted_Player(plr) && !isPaused_Player(plr))) { + interval = 1000 / 15; } - if (isStarted_Player(plr) && !isPaused_Player(plr)) { - refresh_Widget(d); - addTicker_App(animatePlayingAudio_DocumentWidget_, d); + } + return interval; +} + +static uint32_t postPlayerUpdate_DocumentWidget_(uint32_t interval, void *context) { + /* Called in timer thread; don't access the widget. */ + iUnused(context); + postCommand_App("media.player.update"); + return interval; +} + +static void updatePlayers_DocumentWidget_(iDocumentWidget *d) { + if (document_App() == d) { + refresh_Widget(d); + iConstForEach(PtrArray, i, &d->visiblePlayers) { + const iGmRun *run = i.ptr; + iPlayer * plr = audioPlayer_Media(media_GmDocument(d->doc), run->audioId); + if (idleTimeMs_Player(plr) > 3000 && ~flags_Player(plr) & volumeGrabbed_PlayerFlag && + flags_Player(plr) & adjustingVolume_PlayerFlag) { + setFlags_Player(plr, adjustingVolume_PlayerFlag, iFalse); + } } } + if (d->playerTimer && playerUpdateInterval_DocumentWidget_(d) == 0) { + SDL_RemoveTimer(d->playerTimer); + d->playerTimer = 0; + } +} + +static void animatePlayers_DocumentWidget_(iDocumentWidget *d) { + if (document_App() != d) { + if (d->playerTimer) { + SDL_RemoveTimer(d->playerTimer); + d->playerTimer = 0; + } + return; + } + uint32_t interval = playerUpdateInterval_DocumentWidget_(d); + if (interval && !d->playerTimer) { + d->playerTimer = SDL_AddTimer(interval, postPlayerUpdate_DocumentWidget_, d); + } } static void updateVisible_DocumentWidget_(iDocumentWidget *d) { @@ -479,7 +512,7 @@ static void updateVisible_DocumentWidget_(iDocumentWidget *d) { setRange_ScrollWidget(d->scroll, (iRangei){ 0, scrollMax_DocumentWidget_(d) }); const int docSize = size_GmDocument(d->doc).y; setThumb_ScrollWidget(d->scroll, - d->scrollY, + value_Anim(&d->scrollY), docSize > 0 ? height_Rect(bounds) * size_Range(&visRange) / docSize : 0); clear_PtrArray(&d->visibleLinks); clear_PtrArray(&d->visiblePlayers); @@ -487,7 +520,7 @@ static void updateVisible_DocumentWidget_(iDocumentWidget *d) { render_GmDocument(d->doc, visRange, addVisible_DocumentWidget_, d); updateHover_DocumentWidget_(d, mouseCoord_Window(get_Window())); updateSideOpacity_DocumentWidget_(d, iTrue); - animatePlayingAudio_DocumentWidget_(d); + animatePlayers_DocumentWidget_(d); /* Remember scroll positions of recently visited pages. */ { iRecentUrl *recent = mostRecentUrl_History(d->mod.history); if (recent && docSize && d->state == ready_RequestState) { @@ -609,14 +642,9 @@ static void updateOutline_DocumentWidget_(iDocumentWidget *d) { if (head->level == 0) { pos.y += gap_UI * 1.5f; } - pushBack_Array(&d->outline, - &(iOutlineItem){ head->text, - uiLabel_FontId, - (iRect){ addX_I2(pos, indent), size }, - head->level == 0 ? tmHeading1_ColorId - : head->level == 1 ? tmHeading2_ColorId - : tmHeading3_ColorId, - head->level == 0 ? tmQuoteIcon_ColorId : none_ColorId }); + pushBack_Array( + &d->outline, + &(iOutlineItem){ head->text, uiLabel_FontId, (iRect){ addX_I2(pos, indent), size } }); pos.y += size.y; } } @@ -674,8 +702,7 @@ static void showErrorPage_DocumentWidget_(iDocumentWidget *d, enum iGmStatusCode } } setSource_DocumentWidget_(d, src); - resetSmoothScroll_DocumentWidget_(d); - d->scrollY = 0; + init_Anim(&d->scrollY, 0); init_Anim(&d->sideOpacity, 0); d->state = ready_RequestState; } @@ -827,8 +854,6 @@ static void fetch_DocumentWidget_(iDocumentWidget *d) { } static void updateTrust_DocumentWidget_(iDocumentWidget *d, const iGmResponse *response) { -#define openLock_CStr "\U0001f513" -#define closedLock_CStr "\U0001f512" if (response) { d->certFlags = response->certFlags; d->certExpiry = response->certValidUntil; @@ -879,7 +904,7 @@ static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) { d->sourceTime = resp->when; set_Block(&d->sourceContent, &resp->body); updateDocument_DocumentWidget_(d, resp, iTrue); - d->scrollY = d->initNormScrollY * size_GmDocument(d->doc).y; + init_Anim(&d->scrollY, d->initNormScrollY * size_GmDocument(d->doc).y); d->state = ready_RequestState; updateSideOpacity_DocumentWidget_(d, iFalse); updateOutline_DocumentWidget_(d); @@ -893,62 +918,44 @@ static iBool updateFromHistory_DocumentWidget_(iDocumentWidget *d) { return iFalse; } -static void scroll_DocumentWidget_(iDocumentWidget *d, int offset) { - d->scrollY += offset; - if (d->scrollY < 0) { - d->scrollY = 0; +static void refreshWhileScrolling_DocumentWidget_(iAny *ptr) { + iDocumentWidget *d = ptr; + updateVisible_DocumentWidget_(d); + refresh_Widget(d); + if (!isFinished_Anim(&d->scrollY)) { + addTicker_App(refreshWhileScrolling_DocumentWidget_, d); + } +} + +static void smoothScroll_DocumentWidget_(iDocumentWidget *d, int offset, int duration) { + int destY = targetValue_Anim(&d->scrollY) + offset; + if (destY < 0) { + destY = 0; } const int scrollMax = scrollMax_DocumentWidget_(d); if (scrollMax > 0) { - d->scrollY = iMin(d->scrollY, scrollMax); + destY = iMin(destY, scrollMax); } else { - d->scrollY = 0; + destY = 0; } + setValueEased_Anim(&d->scrollY, destY, duration); updateVisible_DocumentWidget_(d); refresh_Widget(as_Widget(d)); -} - -static iBool isSmoothScrolling_DocumentWidget_(const iDocumentWidget *d) { - return d->smoothScroll != 0; -} - -static void doScroll_DocumentWidget_(iAny *ptr) { - iDocumentWidget *d = ptr; - if (!isSmoothScrolling_DocumentWidget_(d)) { - return; /* was cancelled */ - } - const double elapsed = (double) elapsedSinceLastTicker_App() / 1000.0; - int delta = d->smoothSpeed * elapsed * iSign(d->smoothScroll); - if (iAbs(d->smoothScroll) <= iAbs(delta)) { - if (d->smoothContinue) { - d->smoothScroll += d->smoothLastOffset; - } - else { - delta = d->smoothScroll; - } - } - scroll_DocumentWidget_(d, delta); - d->smoothScroll -= delta; - if (isSmoothScrolling_DocumentWidget_(d)) { - addTicker_App(doScroll_DocumentWidget_, d); + if (duration > 0) { + iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iTrue); + addTicker_App(refreshWhileScrolling_DocumentWidget_, d); } } -static void smoothScroll_DocumentWidget_(iDocumentWidget *d, int offset, int speed) { - if (speed == 0) { - scroll_DocumentWidget_(d, offset); - return; - } - d->smoothSpeed = speed; - d->smoothScroll += offset; - d->smoothLastOffset = offset; - addTicker_App(doScroll_DocumentWidget_, d); +static void scroll_DocumentWidget_(iDocumentWidget *d, int offset) { + smoothScroll_DocumentWidget_(d, offset, 0 /* instantly */); } static void scrollTo_DocumentWidget_(iDocumentWidget *d, int documentY, iBool centered) { - d->scrollY = documentY - (centered ? documentBounds_DocumentWidget_(d).size.y / 2 : - lineHeight_Text(paragraph_FontId)); + init_Anim(&d->scrollY, + documentY - (centered ? documentBounds_DocumentWidget_(d).size.y / 2 + : lineHeight_Text(paragraph_FontId))); scroll_DocumentWidget_(d, 0); /* clamp it */ } @@ -983,8 +990,7 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) { break; } case categorySuccess_GmStatusCode: - d->scrollY = 0; - resetSmoothScroll_DocumentWidget_(d); + init_Anim(&d->scrollY, 0); reset_GmDocument(d->doc); /* new content incoming */ updateDocument_DocumentWidget_(d, response_GmRequest(d->request), iTrue); break; @@ -1179,6 +1185,8 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) if (equal_Command(cmd, "window.resized") || equal_Command(cmd, "font.changed")) { const iGmRun *mid = middleRun_DocumentWidget_(d); const char *midLoc = (mid ? mid->text.start : NULL); + /* Alt/Option key may be involved in window size changes. */ + iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iFalse); setWidth_GmDocument( d->doc, documentWidth_DocumentWidget_(d), forceBreakWidth_DocumentWidget_(d)); scroll_DocumentWidget_(d, 0); @@ -1194,6 +1202,10 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) refresh_Widget(w); updateWindowTitle_DocumentWidget_(d); } + else if (equal_Command(cmd, "window.mouse.exited")) { + updateOutlineOpacity_DocumentWidget_(d); + return iFalse; + } else if (equal_Command(cmd, "theme.changed") && document_App() == d) { updateTheme_DocumentWidget_(d); invalidate_DocumentWidget_(d); @@ -1203,7 +1215,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) updateSize_DocumentWidget(d); } else if (equal_Command(cmd, "tabs.changed")) { - d->showLinkNumbers = iFalse; + iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iFalse); if (cmp_String(id_Widget(w), suffixPtr_Command(cmd, "id")) == 0) { /* Set palette for our document. */ updateTheme_DocumentWidget_(d); @@ -1216,7 +1228,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) updateOutlineOpacity_DocumentWidget_(d); updateWindowTitle_DocumentWidget_(d); allocVisBuffer_DocumentWidget_(d); - animatePlayingAudio_DocumentWidget_(d); + animatePlayers_DocumentWidget_(d); return iFalse; } else if (equal_Command(cmd, "server.showcert") && d == document_App()) { @@ -1308,8 +1320,7 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) set_Block(&d->sourceContent, body_GmRequest(d->request)); updateFetchProgress_DocumentWidget_(d); checkResponse_DocumentWidget_(d); - resetSmoothScroll_DocumentWidget_(d); - d->scrollY = d->initNormScrollY * size_GmDocument(d->doc).y; + init_Anim(&d->scrollY, d->initNormScrollY * size_GmDocument(d->doc).y); d->state = ready_RequestState; /* The response may be cached. */ { if (!equal_Rangecc(urlScheme_String(d->mod.url), "about") && @@ -1349,6 +1360,10 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) } } } + else if (equal_Command(cmd, "media.player.update")) { + updatePlayers_DocumentWidget_(d); + return iFalse; + } else if (equal_Command(cmd, "document.stop") && document_App() == d) { if (d->request) { postCommandf_App( @@ -1455,25 +1470,19 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd) return iTrue; } else if (equalWidget_Command(cmd, w, "scroll.moved")) { - d->scrollY = arg_Command(cmd); - resetSmoothScroll_DocumentWidget_(d); + init_Anim(&d->scrollY, arg_Command(cmd)); updateVisible_DocumentWidget_(d); return iTrue; } else if (equalWidget_Command(cmd, w, "scroll.page")) { if (argLabel_Command(cmd, "repeat")) { - if (!d->smoothContinue) { - d->smoothContinue = iTrue; - } - else { - return iTrue; - } + /* TODO: Adjust scroll animation to be linear during repeated scroll? */ } smoothScroll_DocumentWidget_(d, arg_Command(cmd) * (0.5f * height_Rect(documentBounds_DocumentWidget_(d)) - 0 * lineHeight_Text(paragraph_FontId)), - 25 * smoothSpeed_DocumentWidget_); + smoothDuration_DocumentWidget_); return iTrue; } else if (equal_Command(cmd, "document.goto") && document_App() == d) { @@ -1541,9 +1550,9 @@ static size_t visibleLinkOrdinal_DocumentWidget_(const iDocumentWidget *d, iGmLi return iInvalidPos; } -static iRect audioPlayerRect_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run) { +static iRect playerRect_DocumentWidget_(const iDocumentWidget *d, const iGmRun *run) { const iRect docBounds = documentBounds_DocumentWidget_(d); - return moved_Rect(run->bounds, addY_I2(topLeft_Rect(docBounds), -d->scrollY)); + return moved_Rect(run->bounds, addY_I2(topLeft_Rect(docBounds), -value_Anim(&d->scrollY))); } static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *run) { @@ -1567,7 +1576,7 @@ static void setGrabbedPlayer_DocumentWidget_(iDocumentWidget *d, const iGmRun *r } } -static iBool processAudioPlayerEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { +static iBool processPlayerEvents_DocumentWidget_(iDocumentWidget *d, const SDL_Event *ev) { if (ev->type != SDL_MOUSEBUTTONDOWN && ev->type != SDL_MOUSEBUTTONUP && ev->type != SDL_MOUSEMOTION) { return iFalse; @@ -1584,7 +1593,7 @@ static iBool processAudioPlayerEvents_DocumentWidget_(iDocumentWidget *d, const const iInt2 mouse = init_I2(ev->button.x, ev->button.y); iConstForEach(PtrArray, i, &d->visiblePlayers) { const iGmRun *run = i.ptr; - const iRect rect = audioPlayerRect_DocumentWidget_(d, run); + const iRect rect = playerRect_DocumentWidget_(d, run); iPlayer * plr = audioPlayer_Media(media_GmDocument(d->doc), run->audioId); if (contains_Rect(rect, mouse)) { iPlayerUI ui; @@ -1606,7 +1615,7 @@ static iBool processAudioPlayerEvents_DocumentWidget_(iDocumentWidget *d, const } if (contains_Rect(ui.playPauseRect, mouse)) { setPaused_Player(plr, !isPaused_Player(plr)); - animatePlayingAudio_DocumentWidget_(d); + animatePlayers_DocumentWidget_(d); return iTrue; } else if (contains_Rect(ui.rewindRect, mouse)) { @@ -1622,6 +1631,7 @@ static iBool processAudioPlayerEvents_DocumentWidget_(iDocumentWidget *d, const setFlags_Player(plr, adjustingVolume_PlayerFlag, !(flags_Player(plr) & adjustingVolume_PlayerFlag)); + animatePlayers_DocumentWidget_(d); refresh_Widget(d); return iTrue; } @@ -1665,7 +1675,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e case SDLK_LALT: case SDLK_RALT: if (document_App() == d) { - d->showLinkNumbers = iFalse; + iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iFalse); invalidate_DocumentWidget_(d); refresh_Widget(w); } @@ -1675,15 +1685,15 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e case SDLK_SPACE: case SDLK_UP: case SDLK_DOWN: - d->smoothContinue = iFalse; +// d->smoothContinue = iFalse; break; } } if (ev->type == SDL_KEYDOWN) { const int mods = keyMods_Sym(ev->key.keysym.mod); - const int key = ev->key.keysym.sym; - if (d->showLinkNumbers && ((key >= '1' && key <= '9') || - (key >= 'a' && key <= 'z'))) { + const int key = ev->key.keysym.sym; + if ((d->flags & showLinkNumbers_DocumentWidgetFlag) && + ((key >= '1' && key <= '9') || (key >= 'a' && key <= 'z'))) { const size_t ord = isdigit(key) ? key - SDLK_1 : (key - 'a' + 9); iConstForEach(PtrArray, i, &d->visibleLinks) { const iGmRun *run = i.ptr; @@ -1700,21 +1710,21 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e case SDLK_LALT: case SDLK_RALT: if (document_App() == d) { - d->showLinkNumbers = iTrue; + iChangeFlags(d->flags, showLinkNumbers_DocumentWidgetFlag, iTrue); invalidate_DocumentWidget_(d); refresh_Widget(w); } break; case SDLK_HOME: - d->scrollY = 0; - resetSmoothScroll_DocumentWidget_(d); + init_Anim(&d->scrollY, 0); + invalidate_VisBuf(d->visBuf); scroll_DocumentWidget_(d, 0); updateVisible_DocumentWidget_(d); refresh_Widget(w); return iTrue; case SDLK_END: - d->scrollY = scrollMax_DocumentWidget_(d); - resetSmoothScroll_DocumentWidget_(d); + init_Anim(&d->scrollY, scrollMax_DocumentWidget_(d)); + invalidate_VisBuf(d->visBuf); scroll_DocumentWidget_(d, 0); updateVisible_DocumentWidget_(d); refresh_Widget(w); @@ -1723,15 +1733,15 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e case SDLK_DOWN: if (mods == 0) { if (ev->key.repeat) { - if (!d->smoothContinue) { - d->smoothContinue = iTrue; - } - else return iTrue; +// if (!d->smoothContinue) { +// d->smoothContinue = iTrue; +// } +// else return iTrue; } smoothScroll_DocumentWidget_(d, 3 * lineHeight_Text(paragraph_FontId) * (key == SDLK_UP ? -1 : 1), - gap_Text * smoothSpeed_DocumentWidget_); + /*gap_Text * */smoothDuration_DocumentWidget_); return iTrue; } break; @@ -1781,6 +1791,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e } #if defined (iPlatformApple) /* Momentum scrolling. */ + stop_Anim(&d->scrollY); scroll_DocumentWidget_(d, -ev->wheel.y * get_Window()->pixelRatio * acceleration); #else if (keyMods_Sym(SDL_GetModState()) == KMOD_PRIMARY) { @@ -1790,14 +1801,15 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e smoothScroll_DocumentWidget_( d, -3 * ev->wheel.y * lineHeight_Text(paragraph_FontId) * acceleration, - gap_Text * smoothSpeed_DocumentWidget_ + - (isSmoothScrolling_DocumentWidget_(d) ? d->smoothSpeed : 0)); + smoothDuration_DocumentWidget_ * + (!isFinished_Anim(&d->scrollY) && pos_Anim(&d->scrollY) < 0.25f ? 0.5f : 1.0f)); + /* accelerated speed for repeated wheelings */ #endif - d->noHoverWhileScrolling = iTrue; + iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iTrue); return iTrue; } else if (ev->type == SDL_MOUSEMOTION) { - d->noHoverWhileScrolling = iFalse; + iChangeFlags(d->flags, noHoverWhileScrolling_DocumentWidgetFlag, iFalse); if (isVisible_Widget(d->menu)) { setCursor_Window(get_Window(), SDL_SYSTEM_CURSOR_ARROW); } @@ -1877,28 +1889,28 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e processContextMenuEvent_Widget(d->menu, ev, d->hoverLink = NULL); } } - if (processAudioPlayerEvents_DocumentWidget_(d, ev)) { + if (processPlayerEvents_DocumentWidget_(d, ev)) { return iTrue; } switch (processEvent_Click(&d->click, ev)) { case started_ClickResult: - d->selecting = iFalse; + iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iFalse); return iTrue; case drag_ClickResult: { if (d->grabbedPlayer) { iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), d->grabbedPlayer->audioId); iPlayerUI ui; - init_PlayerUI(&ui, plr, audioPlayerRect_DocumentWidget_(d, d->grabbedPlayer)); + init_PlayerUI(&ui, plr, playerRect_DocumentWidget_(d, d->grabbedPlayer)); float off = (float) delta_Click(&d->click).x / (float) width_Rect(ui.volumeSlider); setVolume_Player(plr, d->grabbedStartVolume + off); refresh_Widget(w); return iTrue; } /* Begin selecting a range of text. */ - if (!d->selecting) { + if (~d->flags & selecting_DocumentWidgetFlag) { setFocus_Widget(NULL); /* TODO: Focus this document? */ - d->selecting = iTrue; + iChangeFlags(d->flags, selecting_DocumentWidgetFlag, iTrue); d->selectMark.start = d->selectMark.end = sourceLoc_DocumentWidget_(d, d->click.startPos); refresh_Widget(w); @@ -2042,7 +2054,8 @@ static void fillRange_DrawContext_(iDrawContext *d, const iGmRun *run, enum iCol if (w > width_Rect(run->visBounds) - x) { w = width_Rect(run->visBounds) - x; } - const iInt2 visPos = add_I2(run->bounds.pos, addY_I2(d->viewPos, -d->widget->scrollY)); + const iInt2 visPos = + add_I2(run->bounds.pos, addY_I2(d->viewPos, -value_Anim(&d->widget->scrollY))); fillRect_Paint(&d->paint, (iRect){ addX_I2(visPos, x), init_I2(w, height_Rect(run->bounds)) }, color); } @@ -2071,7 +2084,6 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { } else if (run->audioId) { /* Audio player UI is drawn afterwards as a dynamic overlay. */ - //fillRect_Paint(&d->paint, moved_Rect(run->visBounds, origin), red_ColorId); return; } enum iColorId fg = run->color; @@ -2136,6 +2148,12 @@ static void drawRun_DrawContext_(void *context, const iGmRun *run) { goto runDrawn; } } + if (run->flags & quoteBorder_GmRunFlag) { + drawVLine_Paint(&d->paint, + addX_I2(visPos, -gap_Text * 5 / 2), + height_Rect(run->visBounds), + tmQuoteIcon_ColorId); + } drawRange_Text(run->font, visPos, fg, run->text); // printf("{%s}\n", cstr_Rangecc(run->text)); runDrawn:; @@ -2334,13 +2352,15 @@ static void drawSideElements_DocumentWidget_(const iDocumentWidget *d) { collect_String(format_Time(&d->sourceTime, "Received at %I:%M %p\non %b %d, %Y")); const iInt2 size = advanceRange_Text(font, range_String(recv)); if (size.x <= avail) { - drawString_Text(font, - add_I2(bottomLeft_Rect(bounds), - init_I2(margin, - -margin + -size.y + - iMax(0, scrollMax_DocumentWidget_(d) - d->scrollY))), - tmQuoteIcon_ColorId, - recv); + drawString_Text( + font, + add_I2( + bottomLeft_Rect(bounds), + init_I2(margin, + -margin + -size.y + + iMax(0, scrollMax_DocumentWidget_(d) - value_Anim(&d->scrollY)))), + tmQuoteIcon_ColorId, + recv); } } /* Outline on the right side. */ @@ -2353,9 +2373,9 @@ static void drawSideElements_DocumentWidget_(const iDocumentWidget *d) { const int scrollMax = scrollMax_DocumentWidget_(d); const int outHeight = outlineHeight_DocumentWidget_(d); const int oversize = outHeight - height_Rect(bounds) + topMargin + bottomMargin; - const int scroll = - (oversize > 0 && scrollMax > 0 ? oversize * d->scrollY / scrollMax_DocumentWidget_(d) - : 0); + const int scroll = (oversize > 0 && scrollMax > 0 + ? oversize * value_Anim(&d->scrollY) / scrollMax_DocumentWidget_(d) + : 0); iInt2 pos = add_I2(topRight_Rect(bounds), init_I2(-outWidth - width_Widget(d->scroll), topMargin)); /* Center short outlines vertically. */ @@ -2371,17 +2391,27 @@ static void drawSideElements_DocumentWidget_(const iDocumentWidget *d) { init_I2(outWidth, outHeight + outlinePadding_DocumentWidget_ * gap_UI * 1.5f) }; fillRect_Paint(&p, outlineFrame, tmBannerBackground_ColorId); - const int textFg = drawSideRect_(&p, outlineFrame); //, 1); + drawSideRect_(&p, outlineFrame); + iBool wasAbove = iTrue; iConstForEach(Array, i, &d->outline) { const iOutlineItem *item = i.value; iInt2 visPos = addX_I2(add_I2(pos, item->rect.pos), outlinePadding_DocumentWidget_ * gap_UI); const iBool isVisible = d->lastVisibleRun && d->lastVisibleRun->text.start >= item->text.start; - const int fg = index_ArrayConstIterator(&i) == 0 || isVisible ? textFg - : tmQuoteIcon_ColorId; + const int fg = index_ArrayConstIterator(&i) == 0 || isVisible ? tmOutlineHeadingAbove_ColorId + : tmOutlineHeadingBelow_ColorId; + if (fg == tmOutlineHeadingBelow_ColorId) { + if (wasAbove) { + drawHLine_Paint(&p, + init_I2(left_Rect(outlineFrame), visPos.y - 1), + width_Rect(outlineFrame), + tmOutlineHeadingBelow_ColorId); + wasAbove = iFalse; + } + } drawWrapRange_Text( item->font, visPos, innerWidth - left_Rect(item->rect), fg, item->text); if (left_Rect(item->rect) > 0) { - drawRange_Text(item->font, addX_I2(visPos, -3 * gap_UI), fg, range_CStr("\u2013")); + drawRange_Text(item->font, addX_I2(visPos, -2.75f * gap_UI), fg, range_CStr("\u2022")); } } setOpacity_Text(1.0f); @@ -2390,11 +2420,11 @@ static void drawSideElements_DocumentWidget_(const iDocumentWidget *d) { unsetClip_Paint(&p); } -static void drawAudioPlayers_DocumentWidget_(const iDocumentWidget *d, iPaint *p) { +static void drawPlayers_DocumentWidget_(const iDocumentWidget *d, iPaint *p) { iConstForEach(PtrArray, i, &d->visiblePlayers) { const iGmRun * run = i.ptr; const iPlayer *plr = audioPlayer_Media(media_GmDocument(d->doc), run->audioId); - const iRect rect = audioPlayerRect_DocumentWidget_(d, run); + const iRect rect = playerRect_DocumentWidget_(d, run); iPlayerUI ui; init_PlayerUI(&ui, plr, rect); draw_PlayerUI(&ui, p); @@ -2412,7 +2442,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { const iRect docBounds = documentBounds_DocumentWidget_(d); iDrawContext ctx = { .widget = d, - .showLinkNumbers = d->showLinkNumbers, + .showLinkNumbers = (d->flags & showLinkNumbers_DocumentWidgetFlag) != 0, }; /* Currently visible region. */ const iRangei vis = visibleRange_DocumentWidget_(d); @@ -2465,7 +2495,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { clear_PtrSet(d->invalidRuns); } setClip_Paint(&ctx.paint, bounds); - const int yTop = docBounds.pos.y - d->scrollY; + const int yTop = docBounds.pos.y - value_Anim(&d->scrollY); draw_VisBuf(visBuf, init_I2(bounds.pos.x, yTop)); /* Text markers. */ if (!isEmpty_Range(&d->foundMark) || !isEmpty_Range(&d->selectMark)) { @@ -2476,7 +2506,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) { render_GmDocument(d->doc, vis, drawMark_DrawContext_, &ctx); SDL_SetRenderDrawBlendMode(renderer_Window(get_Window()), SDL_BLENDMODE_NONE); } - drawAudioPlayers_DocumentWidget_(d, &ctx.paint); + drawPlayers_DocumentWidget_(d, &ctx.paint); unsetClip_Paint(&ctx.paint); /* Fill the top and bottom, in case the document is short. */ if (yTop > top_Rect(bounds)) { @@ -2530,7 +2560,6 @@ const iString *bookmarkTitle_DocumentWidget(const iDocumentWidget *d) { return collect_String(joinCStr_StringArray(title, " \u2014 ")); } - void serializeState_DocumentWidget(const iDocumentWidget *d, iStream *outs) { serialize_Model(&d->mod, outs); } @@ -2577,8 +2606,6 @@ void setRedirectCount_DocumentWidget(iDocumentWidget *d, int count) { } iBool isRequestOngoing_DocumentWidget(const iDocumentWidget *d) { - /*return d->state == fetching_RequestState || - d->state == receivedPartialResponse_RequestState;*/ return d->request != NULL; } diff --git a/src/ui/indicatorwidget.c b/src/ui/indicatorwidget.c new file mode 100644 index 000000000..d43e23d95 --- /dev/null +++ b/src/ui/indicatorwidget.c @@ -0,0 +1,158 @@ +/* Copyright 2020 Jaakko Keränen + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +#include "indicatorwidget.h" +#include "paint.h" +#include "util.h" +#include "app.h" +#include "command.h" + +#include + +static int timerId_; /* common timer for all indicators */ +static int animCount_; /* number of animating indicators */ + +static uint32_t postRefresh_(uint32_t interval, void *context) { + iUnused(context); + postRefresh_App(); + return interval; +} + +static void startTimer_(void) { + animCount_++; + if (!timerId_) { + timerId_ = SDL_AddTimer(1000 / 60, postRefresh_, NULL); + } +} + +static void stopTimer_(void) { + iAssert(animCount_ > 0); + if (--animCount_ == 0) { + iAssert(timerId_); + SDL_RemoveTimer(timerId_); + timerId_ = 0; + } +} + +struct Impl_IndicatorWidget{ + iWidget widget; + iAnim pos; +}; + +iDefineObjectConstruction(IndicatorWidget) + +iLocalDef iBool isActive_IndicatorWidget_(const iIndicatorWidget *d) { + return isSelected_Widget(d); +} + +static void setActive_IndicatorWidget_(iIndicatorWidget *d, iBool set) { + setFlags_Widget(as_Widget(d), selected_WidgetFlag, set); +} + +void init_IndicatorWidget(iIndicatorWidget *d) { + iWidget *w = &d->widget; + init_Widget(w); + init_Anim(&d->pos, 0); +} + +static void startTimer_IndicatorWidget_(iIndicatorWidget *d) { + if (!isActive_IndicatorWidget_(d)) { + startTimer_(); + setActive_IndicatorWidget_(d, iTrue); + } +} + +static void stopTimer_IndicatorWidget_(iIndicatorWidget *d) { + if (isActive_IndicatorWidget_(d)) { + stopTimer_(); + setActive_IndicatorWidget_(d, iFalse); + } +} + +void deinit_IndicatorWidget(iIndicatorWidget *d) { + stopTimer_IndicatorWidget_(d); +} + +static iBool isCompleted_IndicatorWidget_(const iIndicatorWidget *d) { + return targetValue_Anim(&d->pos) == 1.0f; +} + +void draw_IndicatorWidget_(const iIndicatorWidget *d) { + const float pos = value_Anim(&d->pos); + if (pos > 0.0f && pos < 1.0f) { + const iWidget *w = &d->widget; + const iRect rect = innerBounds_Widget(w); + iPaint p; + init_Paint(&p); + drawHLine_Paint(&p, + topLeft_Rect(rect), + pos * width_Rect(rect), + isCompleted_IndicatorWidget_(d) ? uiTextAction_ColorId + : uiTextCaution_ColorId); + } +} + +iBool processEvent_IndicatorWidget_(iIndicatorWidget *d, const SDL_Event *ev) { + iWidget *w = &d->widget; + if (ev->type == SDL_USEREVENT && ev->user.code == refresh_UserEventCode) { + if (isFinished_Anim(&d->pos)) { + stopTimer_IndicatorWidget_(d); + } + } + else if (isCommand_SDLEvent(ev)) { + const char *cmd = command_UserEvent(ev); + if (startsWith_CStr(cmd, "document.request.")) { + if (pointerLabel_Command(cmd, "doc") == parent_Widget(w)) { + cmd += 17; + if (equal_Command(cmd, "started")) { + setValue_Anim(&d->pos, 0, 0); + setValue_Anim(&d->pos, 0.75f, 4000); + setFlags_Anim(&d->pos, easeOut_AnimFlag, iTrue); + startTimer_IndicatorWidget_(d); + } + else if (equal_Command(cmd, "finished")) { + if (value_Anim(&d->pos) > 0.01f) { + setValue_Anim(&d->pos, 1.0f, 250); + setFlags_Anim(&d->pos, easeOut_AnimFlag, iFalse); + startTimer_IndicatorWidget_(d); + } + else { + setValue_Anim(&d->pos, 0, 0); + stopTimer_IndicatorWidget_(d); + refresh_Widget(d); + } + } + else if (equal_Command(cmd, "cancelled")) { + setValue_Anim(&d->pos, 0, 0); + stopTimer_IndicatorWidget_(d); + refresh_Widget(d); + } + } + } + } + return iFalse; +} + +iBeginDefineSubclass(IndicatorWidget, Widget) + .draw = (iAny *) draw_IndicatorWidget_, + .processEvent = (iAny *) processEvent_IndicatorWidget_, +iEndDefineSubclass(IndicatorWidget) diff --git a/src/ui/indicatorwidget.h b/src/ui/indicatorwidget.h new file mode 100644 index 000000000..a3d9af390 --- /dev/null +++ b/src/ui/indicatorwidget.h @@ -0,0 +1,28 @@ +/* Copyright 2020 Jaakko Keränen + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +#pragma once + +#include "widget.h" + +iDeclareWidgetClass(IndicatorWidget) +iDeclareObjectConstruction(IndicatorWidget) diff --git a/src/ui/playerui.c b/src/ui/playerui.c index fadbc2da1..3e22a1d21 100644 --- a/src/ui/playerui.c +++ b/src/ui/playerui.c @@ -145,7 +145,7 @@ void draw_PlayerUI(iPlayerUI *d, iPaint *p) { const int dotWidth = advance_Text(uiLabel_FontId, dot).x; draw_Text(uiLabel_FontId, init_I2(s1 * (1.0f - normPos) + s2 * normPos - dotWidth / 2, yMid - hgt / 2), - bright, + isPaused_Player(d->player) ? dim : bright, dot); /* Volume adjustment. */ if (isAdjusting) { diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index b84ae65ef..d140948ef 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -259,7 +259,7 @@ iBool setMode_SidebarWidget(iSidebarWidget *d, enum iSidebarMode mode) { } const float heights[max_SidebarMode] = { 1.333f, 1.333f, 3.5f, 1.2f }; setBackgroundColor_Widget(as_Widget(d->list), - d->mode == documentOutline_SidebarMode ? tmBackground_ColorId + d->mode == documentOutline_SidebarMode ? tmBannerBackground_ColorId : uiBackground_ColorId); setItemHeight_ListWidget(d->list, heights[mode] * lineHeight_Text(uiContent_FontId)); /* Restore previous scroll position. */ diff --git a/src/ui/text.c b/src/ui/text.c index 08c89e47b..a592a97a8 100644 --- a/src/ui/text.c +++ b/src/ui/text.c @@ -80,9 +80,9 @@ iChar char_Glyph(const iGlyph *d) { iDefineTypeConstructionArgs(Glyph, (iChar ch), ch) -/*-----------------------------------------------------------------------------------------------*/ + /*-----------------------------------------------------------------------------------------------*/ -struct Impl_Font { + struct Impl_Font { iBlock * data; stbtt_fontinfo font; float scale; @@ -92,8 +92,9 @@ struct Impl_Font { iHash glyphs; iBool isMonospaced; iBool manualKernOnly; - enum iFontId symbolsFont; /* font to use for symbols */ + enum iFontId symbolsFont; /* font to use for symbols */ enum iFontId japaneseFont; /* font to use for Japanese glyphs */ + enum iFontId koreanFont; /* font to use for Korean glyphs */ uint32_t indexTable[128 - 32]; }; @@ -109,9 +110,10 @@ static void init_Font(iFont *d, const iBlock *data, int height, float scale, enu d->vertOffset = height * (1.0f - scale) / 2; int ascent; stbtt_GetFontVMetrics(&d->font, &ascent, NULL, NULL); - d->baseline = (int) ascent * d->scale; - d->symbolsFont = symbolsFont; + d->baseline = (int) ascent * d->scale; + d->symbolsFont = symbolsFont; d->japaneseFont = regularJapanese_FontId; + d->koreanFont = regularKorean_FontId; d->isMonospaced = iFalse; memset(d->indexTable, 0xff, sizeof(d->indexTable)); } @@ -247,6 +249,15 @@ static void initFonts_Text_(iText *d) { { &fontKosugiMaruRegular_Embedded, textSize * 1.333f, 1.0f, bigSymbols_FontId }, { &fontKosugiMaruRegular_Embedded, textSize * 1.666f, 1.0f, largeSymbols_FontId }, { &fontKosugiMaruRegular_Embedded, textSize * 2.000f, 1.0f, hugeSymbols_FontId }, + /* korean fonts */ + { &fontNanumGothicRegular_Embedded, fontSize_UI, 1.0f, defaultSymbols_FontId }, + { &fontNanumGothicRegular_Embedded, monoSize * 0.750, 1.0f, monospaceSmallSymbols_FontId }, + { &fontNanumGothicRegular_Embedded, monoSize, 1.0f, monospaceSymbols_FontId }, + { &fontNanumGothicRegular_Embedded, textSize, 1.0f, symbols_FontId }, + { &fontNanumGothicRegular_Embedded, textSize * 1.200f, 1.0f, mediumSymbols_FontId }, + { &fontNanumGothicRegular_Embedded, textSize * 1.333f, 1.0f, bigSymbols_FontId }, + { &fontNanumGothicRegular_Embedded, textSize * 1.666f, 1.0f, largeSymbols_FontId }, + { &fontNanumGothicRegular_Embedded, textSize * 2.000f, 1.0f, hugeSymbols_FontId }, }; iForIndices(i, fontData) { iFont *font = &d->fonts[i]; @@ -262,7 +273,6 @@ static void initFonts_Text_(iText *d) { /* Japanese script. */ { /* Everything defaults to the regular sized japanese font, so these are just the other sizes. */ - /* TODO: Add these to the table above... */ font_Text_(default_FontId)->japaneseFont = defaultJapanese_FontId; font_Text_(defaultMedium_FontId)->japaneseFont = defaultJapanese_FontId; font_Text_(defaultMonospace_FontId)->japaneseFont = defaultJapanese_FontId; @@ -270,11 +280,22 @@ static void initFonts_Text_(iText *d) { font_Text_(monospace_FontId)->japaneseFont = monospaceJapanese_FontId; font_Text_(medium_FontId)->japaneseFont = mediumJapanese_FontId; font_Text_(big_FontId)->japaneseFont = bigJapanese_FontId; -// font_Text_(bigBold_FontId)->japaneseFont = bigJapanese_FontId; font_Text_(largeBold_FontId)->japaneseFont = largeJapanese_FontId; font_Text_(largeLight_FontId)->japaneseFont = largeJapanese_FontId; font_Text_(hugeBold_FontId)->japaneseFont = hugeJapanese_FontId; } + /* Korean script. */ { + font_Text_(default_FontId)->koreanFont = defaultKorean_FontId; + font_Text_(defaultMedium_FontId)->koreanFont = defaultKorean_FontId; + font_Text_(defaultMonospace_FontId)->koreanFont = defaultKorean_FontId; + font_Text_(monospaceSmall_FontId)->koreanFont = monospaceSmallKorean_FontId; + font_Text_(monospace_FontId)->koreanFont = monospaceKorean_FontId; + font_Text_(medium_FontId)->koreanFont = mediumKorean_FontId; + font_Text_(big_FontId)->koreanFont = bigKorean_FontId; + font_Text_(largeBold_FontId)->koreanFont = largeKorean_FontId; + font_Text_(largeLight_FontId)->koreanFont = largeKorean_FontId; + font_Text_(hugeBold_FontId)->koreanFont = hugeKorean_FontId; + } gap_Text = iRound(gap_UI * d->contentFontSize); } @@ -491,6 +512,13 @@ iLocalDef iFont *characterFont_Font_(iFont *d, iChar ch, uint32_t *glyphIndex) { return emoji; } } + /* Could be Korean. */ + if (ch > 0x3000) { + iFont *korean = font_Text_(d->koreanFont); + if (korean != d && (*glyphIndex = glyphIndex_Font_(korean, ch)) != 0) { + return korean; + } + } /* Japanese perhaps? */ if (ch > 0x3040) { iFont *japanese = font_Text_(d->japaneseFont); @@ -501,6 +529,9 @@ iLocalDef iFont *characterFont_Font_(iFont *d, iChar ch, uint32_t *glyphIndex) { /* Fall back to Symbola for anything else. */ iFont *font = font_Text_(d->symbolsFont); *glyphIndex = glyphIndex_Font_(font, ch); +// if (!*glyphIndex) { +// fprintf(stderr, "failed to find %08x (%lc)\n", ch, ch); fflush(stderr); +// } return font; } diff --git a/src/ui/text.h b/src/ui/text.h index 87f69300b..35f485280 100644 --- a/src/ui/text.h +++ b/src/ui/text.h @@ -72,6 +72,15 @@ enum iFontId { bigJapanese_FontId, largeJapanese_FontId, hugeJapanese_FontId, + /* korean script */ + defaultKorean_FontId, + monospaceSmallKorean_FontId, + monospaceKorean_FontId, + regularKorean_FontId, + mediumKorean_FontId, + bigKorean_FontId, + largeKorean_FontId, + hugeKorean_FontId, max_FontId, /* Meta: */ diff --git a/src/ui/util.c b/src/ui/util.c index c99df1fd7..603b3213f 100644 --- a/src/ui/util.c +++ b/src/ui/util.c @@ -135,30 +135,98 @@ iBool isFinished_Anim(const iAnim *d) { } void init_Anim(iAnim *d, float value) { - d->due = d->when = frameTime_Window(get_Window()); + d->due = d->when = SDL_GetTicks(); d->from = d->to = value; + d->flags = 0; +} + +iLocalDef float pos_Anim_(const iAnim *d, uint32_t now) { + return (float) (now - d->when) / (float) (d->due - d->when); +} + +iLocalDef float easeIn_(float t) { + return t * t; +} + +iLocalDef float easeOut_(float t) { + return t * (2.0f - t); +} + +iLocalDef float easeBoth_(float t) { + if (t < 0.5f) { + return easeIn_(t * 2.0f) * 0.5f; + } + return 0.5f + easeOut_((t - 0.5f) * 2.0f) * 0.5f; +} + +static float valueAt_Anim_(const iAnim *d, const uint32_t now) { + if (now >= d->due) { + return d->to; + } + if (now <= d->when) { + return d->from; + } + float t = pos_Anim_(d, now); + if ((d->flags & easeBoth_AnimFlag) == easeBoth_AnimFlag) { + t = easeBoth_(t); + } + else if (d->flags & easeIn_AnimFlag) { + t = easeIn_(t); + } + else if (d->flags & easeOut_AnimFlag) { + t = easeOut_(t); + } + return d->from * (1.0f - t) + d->to * t; } void setValue_Anim(iAnim *d, float to, uint32_t span) { - if (fabsf(to - d->to) > 0.00001f) { + if (span == 0) { + d->from = d->to = to; + d->when = d->due = SDL_GetTicks(); + } + else if (fabsf(to - d->to) > 0.00001f) { const uint32_t now = SDL_GetTicks(); - d->from = value_Anim(d); + d->from = valueAt_Anim_(d, now); d->to = to; d->when = now; d->due = now + span; } } -float value_Anim(const iAnim *d) { - const uint32_t now = frameTime_Window(get_Window()); - if (now >= d->due) { - return d->to; +void setValueEased_Anim(iAnim *d, float to, uint32_t span) { + if (fabsf(to - d->to) <= 0.00001f) { + d->to = to; /* Pretty much unchanged. */ + return; } - if (now <= d->when) { - return d->from; + const uint32_t now = SDL_GetTicks(); + if (isFinished_Anim(d)) { + d->from = d->to; + d->flags = easeBoth_AnimFlag; + } + else { + d->from = valueAt_Anim_(d, now); + d->flags = easeOut_AnimFlag; } - const float pos = (float) (now - d->when) / (float) (d->due - d->when); - return d->from * (1.0f - pos) + d->to * pos; + d->to = to; + d->when = now; + d->due = now + span; +} + +void setFlags_Anim(iAnim *d, int flags, iBool set) { + iChangeFlags(d->flags, flags, set); +} + +void stop_Anim(iAnim *d) { + d->from = d->to = value_Anim(d); + d->when = d->due = SDL_GetTicks(); +} + +float pos_Anim(const iAnim *d) { + return pos_Anim_(d, frameTime_Window(get_Window())); +} + +float value_Anim(const iAnim *d) { + return valueAt_Anim_(d, frameTime_Window(get_Window())); } /*-----------------------------------------------------------------------------------------------*/ @@ -931,8 +999,42 @@ iWidget *makePreferences_Widget(void) { addChild_Widget(headings, iClob(makeHeading_Widget("UI scale factor:"))); setId_Widget(addChild_Widget(values, iClob(new_InputWidget(8))), "prefs.uiscale"); } + /* Colors. */ { + appendTwoColumnPage_(tabs, "Colors", '2', &headings, &values); + makeTwoColumnHeading_("PAGE CONTENTS", headings, values); + for (int i = 0; i < 2; ++i) { + const iBool isDark = (i == 0); + const char *mode = isDark ? "dark" : "light"; + const iMenuItem themes[] = { + { "Colorful Dark", 0, 0, format_CStr("doctheme.%s.set arg:%d", mode, colorfulDark_GmDocumentTheme) }, + { "Colorful Light", 0, 0, format_CStr("doctheme.%s.set arg:%d", mode, colorfulLight_GmDocumentTheme) }, + { "Black", 0, 0, format_CStr("doctheme.%s.set arg:%d", mode, black_GmDocumentTheme) }, + { "Gray", 0, 0, format_CStr("doctheme.%s.set arg:%d", mode, gray_GmDocumentTheme) }, + { "Sepia", 0, 0, format_CStr("doctheme.%s.set arg:%d", mode, sepia_GmDocumentTheme) }, + { "White", 0, 0, format_CStr("doctheme.%s.set arg:%d", mode, white_GmDocumentTheme) }, + { "High Contrast", 0, 0, format_CStr("doctheme.%s.set arg:%d", mode, highContrast_GmDocumentTheme) }, + }; + addChild_Widget(headings, iClob(makeHeading_Widget(isDark ? "Dark theme:" : "Light theme:"))); + setId_Widget(addChild_Widget(values, + iClob(makeMenuButton_LabelWidget( + themes[0].label, themes, iElemCount(themes)))), + format_CStr("prefs.doctheme.%s", mode)); + } + //addChild_Widget(values, iClob(new_LabelWidget("Colorful", 0, 0, 0))); +// addChild_Widget(headings, iClob(makeHeading_Widget("Light theme:"))); +// addChild_Widget(values, iClob(new_LabelWidget("White", 0, 0, 0))); + addChild_Widget(headings, iClob(makeHeading_Widget("Saturation:"))); + iWidget *sats = new_Widget(); + /* Saturation levels. */ { + addRadioButton_(sats, "prefs.saturation.3", "Full", "saturation.set arg:100"); + addRadioButton_(sats, "prefs.saturation.2", "Reduced", "saturation.set arg:66"); + addRadioButton_(sats, "prefs.saturation.1", "Minimal", "saturation.set arg:33"); + addRadioButton_(sats, "prefs.saturation.0", "Monochrome", "saturation.set arg:0"); + } + addChildFlags_Widget(values, iClob(sats), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag); + } /* Layout. */ { - appendTwoColumnPage_(tabs, "Style", '2', &headings, &values); + appendTwoColumnPage_(tabs, "Style", '3', &headings, &values); /* Fonts. */ { iWidget *fonts; addChild_Widget(headings, iClob(makeHeading_Widget("Heading font:"))); @@ -957,28 +1059,18 @@ iWidget *makePreferences_Widget(void) { addRadioButton_(widths, "prefs.linewidth.1000", "Window", "linewidth.set arg:1000"); } addChildFlags_Widget(values, iClob(widths), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag); + addChild_Widget(headings, iClob(makeHeading_Widget("Quote indicator:"))); + iWidget *quote = new_Widget(); { + addRadioButton_(quote, "prefs.quoteicon.1", "Icon", "quoteicon.set arg:1"); + addRadioButton_(quote, "prefs.quoteicon.0", "Line", "quoteicon.set arg:0"); + } + addChildFlags_Widget(values, iClob(quote), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag); addChild_Widget(headings, iClob(makeHeading_Widget("Big 1st paragaph:"))); addChild_Widget(values, iClob(makeToggle_Widget("prefs.biglede"))); makeTwoColumnHeading_("WIDE LAYOUT", headings, values); addChild_Widget(headings, iClob(makeHeading_Widget("Site icon:"))); addChild_Widget(values, iClob(makeToggle_Widget("prefs.sideicon"))); } - /* Colors. */ { - appendTwoColumnPage_(tabs, "Colors", '3', &headings, &values); - addChild_Widget(headings, iClob(makeHeading_Widget("Dark theme:"))); - addChild_Widget(values, iClob(new_LabelWidget("Colorful", 0, 0, 0))); - addChild_Widget(headings, iClob(makeHeading_Widget("Light theme:"))); - addChild_Widget(values, iClob(new_LabelWidget("White", 0, 0, 0))); - addChild_Widget(headings, iClob(makeHeading_Widget("Saturation:"))); - iWidget *sats = new_Widget(); - /* Saturation levels. */ { - addRadioButton_(sats, "prefs.saturation.3", "Full", "saturation.set arg:100"); - addRadioButton_(sats, "prefs.saturation.2", "Reduced", "saturation.set arg:66"); - addRadioButton_(sats, "prefs.saturation.1", "Minimal", "saturation.set arg:33"); - addRadioButton_(sats, "prefs.saturation.0", "Monochrome", "saturation.set arg:0"); - } - addChildFlags_Widget(values, iClob(sats), arrangeHorizontal_WidgetFlag | arrangeSize_WidgetFlag); - } /* Proxies. */ { appendTwoColumnPage_(tabs, "Proxies", '4', &headings, &values); addChild_Widget(headings, iClob(makeHeading_Widget("Gopher proxy:"))); diff --git a/src/ui/util.h b/src/ui/util.h index a33bf713b..9796b387c 100644 --- a/src/ui/util.h +++ b/src/ui/util.h @@ -68,15 +68,36 @@ iLocalDef iBool isOverlapping_Rangei(iRangei a, iRangei b) { iDeclareType(Anim) +enum iAnimFlag { + indefinite_AnimFlag = iBit(1), /* does not end; must be linear */ + easeIn_AnimFlag = iBit(2), + easeOut_AnimFlag = iBit(3), + easeBoth_AnimFlag = easeIn_AnimFlag | easeOut_AnimFlag, +}; + struct Impl_Anim { float from, to; uint32_t when, due; + int flags; }; -void init_Anim (iAnim *, float value); -void setValue_Anim (iAnim *, float to, uint32_t span); -float value_Anim (const iAnim *); -iBool isFinished_Anim (const iAnim *); +void init_Anim (iAnim *, float value); +void setValue_Anim (iAnim *, float to, uint32_t span); +void setValueLinear_Anim (iAnim *, float to, uint32_t span); +void setValueEased_Anim (iAnim *, float to, uint32_t span); +void setFlags_Anim (iAnim *, int flags, iBool set); +void stop_Anim (iAnim *); + +iBool isFinished_Anim (const iAnim *); +float pos_Anim (const iAnim *); +float value_Anim (const iAnim *); + +iLocalDef float targetValue_Anim(const iAnim *d) { + return d->to; +} +iLocalDef iBool isLinear_Anim(const iAnim *d) { + return (d->flags & (easeIn_AnimFlag | easeOut_AnimFlag)) == 0; +} /*-----------------------------------------------------------------------------------------------*/ diff --git a/src/ui/widget.h b/src/ui/widget.h index f39612eda..a1a38f28e 100644 --- a/src/ui/widget.h +++ b/src/ui/widget.h @@ -160,6 +160,13 @@ iLocalDef iObjectList *children_Widget(iAnyObject *d) { iAssert(isInstance_Object(d, &Class_Widget)); return ((iWidget *) d)->children; } +iLocalDef iWidget *parent_Widget(const iAnyObject *d) { + if (d) { + iAssert(isInstance_Object(d, &Class_Widget)); + return ((iWidget *) d)->parent; + } + return NULL; +} iBool isVisible_Widget (const iAnyObject *); iBool isDisabled_Widget (const iAnyObject *); diff --git a/src/ui/window.c b/src/ui/window.c index 40215506c..c5194ea03 100644 --- a/src/ui/window.c +++ b/src/ui/window.c @@ -524,6 +524,7 @@ void init_Window(iWindow *d, iRect rect) { d->lastRect = rect; d->pendingCursor = NULL; d->isDrawFrozen = iTrue; + d->isMouseInside = iTrue; uint32_t flags = 0; #if defined (iPlatformApple) SDL_SetHint(SDL_HINT_RENDER_DRIVER, shouldDefaultToMetalRenderer_MacOS() ? "metal" : "opengl"); @@ -543,7 +544,7 @@ void init_Window(iWindow *d, iRect rect) { if (left_Rect(rect) >= 0 || top_Rect(rect) >= 0) { SDL_SetWindowPosition(d->win, left_Rect(rect), top_Rect(rect)); } - const iInt2 minSize = init_I2(425, 250); + const iInt2 minSize = init_I2(425, 300); SDL_SetWindowMinimumSize(d->win, minSize.x, minSize.y); SDL_SetWindowTitle(d->win, "Lagrange"); /* Some info. */ { @@ -655,6 +656,12 @@ static iBool handleWindowEvent_Window_(iWindow *d, const SDL_WindowEvent *ev) { return iTrue; case SDL_WINDOWEVENT_LEAVE: unhover_Widget(); + d->isMouseInside = iFalse; + postCommand_App("window.mouse.exited"); + return iTrue; + case SDL_WINDOWEVENT_ENTER: + d->isMouseInside = iTrue; + postCommand_App("window.mouse.entered"); return iTrue; default: break; @@ -812,6 +819,9 @@ iInt2 coord_Window(const iWindow *d, int x, int y) { } iInt2 mouseCoord_Window(const iWindow *d) { + if (!d->isMouseInside) { + return init_I2(-1000000, -1000000); + } int x, y; SDL_GetMouseState(&x, &y); return coord_Window(d, x, y); diff --git a/src/ui/window.h b/src/ui/window.h index da4a21233..3ede15781 100644 --- a/src/ui/window.h +++ b/src/ui/window.h @@ -37,6 +37,7 @@ struct Impl_Window { iInt2 initialPos; iRect lastRect; /* updated when window is moved/resized */ iBool isDrawFrozen; /* avoids premature draws while restoring window state */ + iBool isMouseInside; SDL_Renderer *render; iWidget * root; float pixelRatio;