├── docs ├── .nojekyll ├── CNAME ├── images │ ├── screenshot-1.png │ ├── screenshot-2.png │ ├── screenshot-3.png │ └── screenshot-4.png ├── css │ ├── index.css │ └── normalize.css ├── index.html └── privacy-policy.html ├── dists ├── 请先读我.txt ├── flipclock.ttf ├── icons │ ├── 64x64 │ │ └── one.alynx.FlipClock.png │ ├── 128x128 │ │ └── one.alynx.FlipClock.png │ └── scalable │ │ └── one.alynx.FlipClock.svg ├── one.alynx.FlipClock.desktop.in ├── COPYING ├── flipclock.conf └── one.alynx.FlipClock.metainfo.xml ├── screenshot.png ├── srcs ├── config.h.in ├── card.h ├── getarg.h ├── clock.h ├── getarg.c ├── flipclock.h ├── main.c ├── clock.c ├── card.c └── flipclock.c ├── .gitignore ├── subprojects ├── sdl2.wrap ├── sdl2_ttf.wrap └── freetype2.wrap ├── Android.mk ├── .clang-format ├── meson.build ├── README.md └── LICENSE /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | flipclock.alynx.one 2 | -------------------------------------------------------------------------------- /dists/请先读我.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlynxZhou/flipclock/HEAD/dists/请先读我.txt -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlynxZhou/flipclock/HEAD/screenshot.png -------------------------------------------------------------------------------- /dists/flipclock.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlynxZhou/flipclock/HEAD/dists/flipclock.ttf -------------------------------------------------------------------------------- /docs/images/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlynxZhou/flipclock/HEAD/docs/images/screenshot-1.png -------------------------------------------------------------------------------- /docs/images/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlynxZhou/flipclock/HEAD/docs/images/screenshot-2.png -------------------------------------------------------------------------------- /docs/images/screenshot-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlynxZhou/flipclock/HEAD/docs/images/screenshot-3.png -------------------------------------------------------------------------------- /docs/images/screenshot-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlynxZhou/flipclock/HEAD/docs/images/screenshot-4.png -------------------------------------------------------------------------------- /dists/icons/64x64/one.alynx.FlipClock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlynxZhou/flipclock/HEAD/dists/icons/64x64/one.alynx.FlipClock.png -------------------------------------------------------------------------------- /dists/icons/128x128/one.alynx.FlipClock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlynxZhou/flipclock/HEAD/dists/icons/128x128/one.alynx.FlipClock.png -------------------------------------------------------------------------------- /srcs/config.h.in: -------------------------------------------------------------------------------- 1 | #ifndef __CONFIG_H__ 2 | 3 | #define PROJECT_VERSION "@project_version@" 4 | #define PACKAGE_BINDIR "@package_bindir@" 5 | #define PACKAGE_DATADIR "@package_datadir@" 6 | #define PACKAGE_SYSCONFDIR "@package_sysconfdir@" 7 | 8 | #endif 9 | -------------------------------------------------------------------------------- /dists/one.alynx.FlipClock.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name=FlipClock 4 | Comment=A flip clock screensaver supported by SDL2. 5 | Icon=one.alynx.FlipClock 6 | Exec=@package_bindir@/flipclock 7 | TryExec=@package_bindir@/flipclock 8 | StartupNotify=false 9 | Terminal=false 10 | Categories=Utility;Clock; 11 | StartupWMClass=flipclock 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Object files 2 | *.o 3 | *.ko 4 | *.obj 5 | *.elf 6 | 7 | # Precompiled Headers 8 | *.gch 9 | *.pch 10 | 11 | # Libraries 12 | #*.lib 13 | *.a 14 | *.la 15 | *.lo 16 | 17 | # Shared objects (inc. Windows DLLs) 18 | *.dll 19 | *.so 20 | *.so.* 21 | *.dylib 22 | 23 | # Executables 24 | *.exe 25 | *.out 26 | *.app 27 | *.i*86 28 | *.x86_64 29 | *.hex 30 | 31 | # Debug files 32 | *.dSYM/ 33 | *.su 34 | 35 | build/ 36 | .cache/ -------------------------------------------------------------------------------- /subprojects/sdl2.wrap: -------------------------------------------------------------------------------- 1 | [wrap-file] 2 | directory = SDL2-2.0.12 3 | source_url = https://www.libsdl.org/release/SDL2-2.0.12.tar.gz 4 | source_filename = SDL2-2.0.12.tar.gz 5 | source_hash = 349268f695c02efbc9b9148a70b85e58cefbbf704abd3e91be654db7f1e2c863 6 | patch_url = https://wrapdb.mesonbuild.com/v1/projects/sdl2/2.0.12/2/get_zip 7 | patch_filename = sdl2-2.0.12-2-wrap.zip 8 | patch_hash = 50a1f974c4521f16f002a6c14d791d905727ba99927037587e6789750eecbac8 9 | 10 | [provide] 11 | sdl2 = sdl2_dep 12 | -------------------------------------------------------------------------------- /subprojects/sdl2_ttf.wrap: -------------------------------------------------------------------------------- 1 | [wrap-file] 2 | directory = SDL2_ttf-2.0.12 3 | source_url = https://www.libsdl.org/projects/SDL_ttf/release/SDL2_ttf-2.0.12.zip 4 | source_filename = SDL2_ttf-2.0.12.zip 5 | source_hash = eb909b4e5717b86e87dad7ee15adbbc5f26a33bd2dc22b93a77517bb0ba7fec8 6 | patch_url = https://wrapdb.mesonbuild.com/v1/projects/sdl2_ttf/2.0.12/3/get_zip 7 | patch_filename = sdl2_ttf-2.0.12-3-wrap.zip 8 | patch_hash = 1113ee5af044f3549daa62047af4a3cb7ba2330acbd78932f4e9d206170073f4 9 | 10 | [provide] 11 | sdl2_ttf = sdl2_ttf_dep 12 | -------------------------------------------------------------------------------- /subprojects/freetype2.wrap: -------------------------------------------------------------------------------- 1 | [wrap-file] 2 | directory = freetype-2.9.1 3 | source_url = https://download.savannah.gnu.org/releases/freetype/freetype-2.9.1.tar.gz 4 | source_filename = freetype-2.9.1.tar.gz 5 | source_hash = ec391504e55498adceb30baceebd147a6e963f636eb617424bcfc47a169898ce 6 | patch_url = https://wrapdb.mesonbuild.com/v1/projects/freetype2/2.9.1/2/get_zip 7 | patch_filename = freetype2-2.9.1-2-wrap.zip 8 | patch_hash = eb47e263df3f69281f96011b16442e7a1352ed5a241c1bdda2d9a86be71cf578 9 | 10 | [provide] 11 | freetype2 = freetype_dep 12 | -------------------------------------------------------------------------------- /Android.mk: -------------------------------------------------------------------------------- 1 | LOCAL_PATH := $(call my-dir) 2 | 3 | include $(CLEAR_VARS) 4 | 5 | LOCAL_MODULE := main 6 | 7 | SDL_PATH := ../SDL 8 | SDL_TTF_PATH := ../SDL_ttf 9 | 10 | LOCAL_C_INCLUDES := $(LOCAL_PATH)/$(SDL_PATH)/include $(LOCAL_PATH)/$(SDL_TTF_PATH)/include 11 | 12 | LOCAL_SRC_FILES := srcs/main.c srcs/getarg.c srcs/card.c srcs/clock.c srcs/flipclock.c 13 | 14 | LOCAL_SHARED_LIBRARIES := SDL2 SDL2_ttf 15 | 16 | LOCAL_LDLIBS := -lGLESv1_CM -lGLESv2 -lOpenSLES -landroid -llog -lm 17 | 18 | ifeq ($(NDK_DEBUG), 1) 19 | LOCAL_CFLAGS += -D__DEBUG__ 20 | endif 21 | 22 | include $(BUILD_SHARED_LIBRARY) 23 | -------------------------------------------------------------------------------- /dists/COPYING: -------------------------------------------------------------------------------- 1 | FlipClock is distributed under the terms of the Apache-2.0 License. 2 | 3 | FlipClock depends on SDL2 and SDL2_ttf, which are distributed under the terms of the Zlib License. 4 | 5 | SDL2_ttf also depends on FreeType, which is distributed under the terms of the FreeType License. 6 | 7 | flipclock.ttf is modified from GNU FreeSans, which is distributed under the terms of the GNU General Public License. 8 | 9 | Please visit those links if you need more info about those licenses: 10 | 11 | https://www.apache.org/licenses/LICENSE-2.0 12 | 13 | https://www.freetype.org/license.html 14 | 15 | http://www.zlib.net/zlib_license.html 16 | 17 | https://www.gnu.org/software/freefont/license.html 18 | -------------------------------------------------------------------------------- /docs/css/index.css: -------------------------------------------------------------------------------- 1 | div.container { 2 | padding: 0; 3 | margin: 0 auto; 4 | width: 75%; 5 | height: 100%; 6 | /* You have a brand screen! You must be so rich but we take care your neck. */ 7 | max-width: 120em; 8 | } 9 | pre.code { 10 | /* Keep Chinese and English the same height. */ 11 | line-height: 1.5; 12 | font-feature-settings: "liga" 0; 13 | white-space: pre-line; 14 | } 15 | div.title { 16 | font: bold 2rem Roboto, sans-serif; 17 | } 18 | div.title a { 19 | color: #000; 20 | text-decoration: none; 21 | border-bottom: none; 22 | } 23 | div.subtitle { 24 | font: italic 1.5rem Roboto, sans-serif; 25 | } 26 | div.screenshots { 27 | text-align: center; 28 | } 29 | .screenshot { 30 | width: 100%; 31 | } 32 | -------------------------------------------------------------------------------- /dists/flipclock.conf: -------------------------------------------------------------------------------- 1 | # Uncomment `ampm = true` to use 12-hour format. 2 | #ampm = true 3 | # Uncomment `full = false` to disable fullscreen. 4 | # You should not change this for a screensaver. 5 | #full = false 6 | # Uncomment `show_second = true` to show second. 7 | #show_second = true 8 | # Uncomment `font = ` and add path to use custom font. 9 | #font = 10 | # Uncomment `text_scale = 0.8` to modify text scale. 11 | # This scales the text again based on card scale. 12 | #text_scale = 0.8 13 | # Uncomment `card_scale = 0.8` to modify card scale. 14 | # This also scales the text. 15 | #card_scale = 0.8 16 | # Uncomment `text_color = ` to modify the color of the text. 17 | #text_color = #000000ff 18 | # Uncomment `box_color = ` to modify the color of the rounded box behind the text. 19 | #box_color = #fe9a00ff 20 | # Uncomment `background_color = ` to modify the color of the background. 21 | #background_color = #000000ff 22 | -------------------------------------------------------------------------------- /srcs/card.h: -------------------------------------------------------------------------------- 1 | #ifndef __CARD_H__ 2 | #define __CARD_H__ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | 9 | // I am not creating a textarea. 10 | #define MAX_TEXT_LENGTH 8 11 | 12 | struct flipclock_card { 13 | struct flipclock *app; 14 | SDL_Renderer *renderer; 15 | SDL_Texture *current; 16 | SDL_Texture *previous; 17 | bool should_redraw; 18 | long long start_tick; 19 | SDL_Rect rect; 20 | char text[MAX_TEXT_LENGTH]; 21 | TTF_Font *font; 22 | bool has_sub_text; 23 | SDL_Rect sub_rect; 24 | char sub_text[MAX_TEXT_LENGTH]; 25 | TTF_Font *sub_font; 26 | int divider_height; 27 | int radius; 28 | }; 29 | 30 | struct flipclock_card *flipclock_card_create(struct flipclock *app, 31 | SDL_Renderer *renderer); 32 | void flipclock_card_set_rect(struct flipclock_card *card, const SDL_Rect rect); 33 | void flipclock_card_set_text(struct flipclock_card *card, const char text[]); 34 | void flipclock_card_set_sub_text(struct flipclock_card *card, 35 | const char sub_text[]); 36 | void flipclock_card_flip(struct flipclock_card *card); 37 | void flipclock_card_animate(struct flipclock_card *card); 38 | void flipclock_card_destory(struct flipclock_card *card); 39 | 40 | #endif 41 | -------------------------------------------------------------------------------- /srcs/getarg.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Alynx Zhou (https://alynx.one/) 3 | */ 4 | #ifndef __GETARG_H__ 5 | #define __GETARG_H__ 6 | 7 | #if defined(_WIN32) 8 | # define OPT_START '/' 9 | #else 10 | # define OPT_START '-' 11 | #endif 12 | 13 | #define FORCE_STOP_OPTS "--" 14 | 15 | // Global optarg. 16 | extern char *argopt; 17 | 18 | /** 19 | * `getarg()`: a portable option parser similiar to `getopt()`. 20 | * 21 | * Just use it like `getopt()`, but with following differences: 22 | * - Global variable `optarg` is replaced with `argopt`. 23 | * - `getopt()` will return `'?'` if it gets an option character not in 24 | * `optstring`, `getarg()` will return the character. 25 | * - `getopt()` will return `'?'` or `':'` if it gets an option character with a 26 | * missing argument string, `getarg()` will return the character, you should 27 | * check whether `argopt` is `NULL` by yourself. 28 | * - `getopt()` will return `-1` if it gets `"--"` and give you a global 29 | * variable `optind` to handle remaining argument strings, `getarg()` does 30 | * not return `-1` on `"--"`, but it will stop option-parsing, you can 31 | * continue calling it in a loop to get next argument strings in `argopt`, 32 | * it will return `0` for them. 33 | */ 34 | int getarg(int argc, char *argv[], const char optstring[]); 35 | 36 | #endif 37 | -------------------------------------------------------------------------------- /srcs/clock.h: -------------------------------------------------------------------------------- 1 | #ifndef __CLOCK_H__ 2 | #define __CLOCK_H__ 3 | 4 | #include 5 | 6 | #include 7 | 8 | struct flipclock_clock { 9 | struct flipclock *app; 10 | SDL_Window *window; 11 | SDL_Renderer *renderer; 12 | struct flipclock_card *hour; 13 | struct flipclock_card *minute; 14 | struct flipclock_card *second; 15 | int i; 16 | int w; 17 | int h; 18 | bool waiting; 19 | }; 20 | 21 | struct flipclock_clock *flipclock_clock_create(struct flipclock *app, int i); 22 | #if defined(_WIN32) 23 | struct flipclock_clock *flipclock_clock_create_preview(struct flipclock *app); 24 | #endif 25 | void flipclock_clock_set_show_second(struct flipclock_clock *clock, 26 | bool show_second); 27 | void flipclock_clock_set_fullscreen(struct flipclock_clock *clock, bool full); 28 | void flipclock_clock_set_hour(struct flipclock_clock *clock, const char hour[], 29 | bool flip); 30 | void flipclock_clock_set_minute(struct flipclock_clock *clock, 31 | const char minute[], bool flip); 32 | void flipclock_clock_set_second(struct flipclock_clock *clock, 33 | const char second[], bool flip); 34 | void flipclock_clock_set_ampm(struct flipclock_clock *clock, const char ampm[]); 35 | void flipclock_clock_handle_window_event(struct flipclock_clock *clock, 36 | SDL_Event event); 37 | void flipclock_clock_animate(struct flipclock_clock *clock); 38 | void flipclock_clock_destroy(struct flipclock_clock *clock); 39 | 40 | #endif 41 | -------------------------------------------------------------------------------- /srcs/getarg.c: -------------------------------------------------------------------------------- 1 | /** 2 | * Alynx Zhou (https://alynx.one/) 3 | */ 4 | #include 5 | 6 | #include "getarg.h" 7 | 8 | char *argopt = NULL; 9 | 10 | // TODO: Make it more similiar to getopt(). 11 | int getarg(int argc, char *argv[], const char optstring[]) 12 | { 13 | static int force_stopped = 0; 14 | // Always init i with 1, because 0 is the program name. 15 | static int i = 1; 16 | // Always init j with 1, because 0 is OPT_START. 17 | static int j = 1; 18 | // Temp i and j for an option followed by a value. 19 | int temp_i = 0; 20 | int temp_j = 0; 21 | 22 | while (i < argc) { 23 | argopt = NULL; 24 | 25 | // `"--"` forces an end of option parsing, all remaining 26 | // arguments are treated as values, so program can handle values 27 | // starting with `'-'`. 28 | if (force_stopped) { 29 | argopt = argv[i]; 30 | ++i; 31 | return 0; 32 | } else if (!strcmp(argv[i], FORCE_STOP_OPTS)) { 33 | force_stopped = 1; 34 | // Skip `"--"`. 35 | ++i; 36 | continue; 37 | } 38 | 39 | // Not force_stopped, do option parsing. 40 | 41 | // A string is finished, jump to next. 42 | if (argv[i][j] == '\0') { 43 | ++i; 44 | j = 1; 45 | continue; 46 | } 47 | 48 | // We jump to a string for the first time and it's just a value. 49 | if (j == 1 && argv[i][0] != OPT_START) { 50 | argopt = argv[i]; 51 | ++i; 52 | return 0; 53 | } 54 | 55 | char *chrptr = strchr(optstring, argv[i][j]); 56 | if (chrptr == NULL || *(chrptr + 1) != ':') { 57 | // Not a valid option, but just return it. Or an option 58 | // without value, which means options not finished. 59 | temp_j = j; 60 | ++j; 61 | return argv[i][temp_j]; 62 | } else { 63 | // With value. 64 | if (argv[i][j + 1] != '\0') { 65 | // gcc style `"-Wall"`. 66 | temp_i = i; 67 | temp_j = j; 68 | argopt = &argv[i][j + 1]; 69 | ++i; 70 | j = 1; 71 | return argv[temp_i][temp_j]; 72 | } else if (i + 1 < argc) { 73 | // `"-W all"`. 74 | temp_i = i; 75 | temp_j = j; 76 | argopt = argv[i + 1]; 77 | // Skip value, so add 2 to i. 78 | i += 2; 79 | j = 1; 80 | return argv[temp_i][temp_j]; 81 | } else { 82 | // Last argument is `"-W"` without any value. 83 | // It's safe to just increase j, we will go into 84 | // `'\0'` to increase i and then break the loop. 85 | temp_j = j; 86 | ++j; 87 | return argv[i][temp_j]; 88 | } 89 | } 90 | } 91 | 92 | return -1; 93 | } 94 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | FlipClock 13 | 14 | 15 |
16 |
17 | 18 |
A flip clock screensaver supported by SDL2.
19 |
20 |
21 |
22 |
23 |

Description

24 |

This is a flip clock simulator, which displays time on digit cards and flips them when time changes.

25 |

Features

26 |
    27 |
  • Support both Linux and Windows.
  • 28 |
  • Using SDL2, not SDL1. Using plain C, not Flash.
  • 29 |
  • Can be used as a Windows screensaver.
  • 30 |
  • Support configuration file and CLI arguments.
  • 31 |
  • Only depends on SDL2, SDL2_ttf and C standard library.
  • 32 |
33 |

Releases

34 |

Please go to GitHub Release Page.

35 |

Windows users please download file with win in its name, extract it and right click flipclock.scr to install it as a screensaver. Please note I may not have time to build every version for Windows, just pick the latest available one.

36 |

Installation

37 |

Please check Installation section of README in GitHub Repo. Because I don't want to manually convert Markdown to HTML here.

38 |

Usage

39 |

Please check Configuration section of README in GitHub Repo. Because I don't want to manually convert Markdown to HTML here. Also you can run flipclock -h to see CLI arguments.

40 |

FAQ

41 |

WIP

42 |

Screenshots

43 |
44 | screenshot-1.png 45 | screenshot-2.png 46 | screenshot-3.png 47 | screenshot-4.png 48 |
49 |
50 |
51 |
52 |
53 |

This page is currently working in progress, for more info please visit GitHub Repo.

54 | Copyright Alynx Zhou 2022 55 |
56 |
57 | 58 | 59 | -------------------------------------------------------------------------------- /dists/one.alynx.FlipClock.metainfo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | one.alynx.FlipClock 5 | 6 | FlipClock 7 | A flip clock screensaver supported by SDL2. 8 | 9 | CC-BY-SA-3.0 10 | Apache-2.0 11 | 12 | 13 | 14 | one.alynx.FlipClock.desktop 15 | Alynx Zhou 16 | 17 | 18 | pointing 19 | keyboard 20 | touch 21 | tablet 22 | 23 | 24 | 25 |

26 | A flip clock screensaver supported by SDL2. It simulates a flip clock, displays time in digit cards and will flip cards if time changes. 27 |

28 |
29 | https://flipclock.alynx.one/ 30 | https://github.com/AlynxZhou/flipclock/issues 31 | https://github.com/AlynxZhou/flipclock 32 | 33 | 34 | https://flipclock.alynx.one/images/screenshot-1.png 35 | 36 | 37 | https://flipclock.alynx.one/images/screenshot-2.png 38 | 39 | 40 | https://flipclock.alynx.one/images/screenshot-3.png 41 | 42 | 43 | https://flipclock.alynx.one/images/screenshot-4.png 44 | 45 | 46 | 47 | 48 | 49 | 50 |

Added card for second.

51 |

Updated touch event handling.

52 |

Updated arguments handling.

53 |
54 |
55 | 56 | 57 |

Fixed Android build.

58 |

Updated SDL2_ttf function call.

59 |
60 |
61 | 62 | 63 |

Updated Android linking libraries.

64 |
65 |
66 | 67 | 68 |

Updated to make flipping card a widget.

69 |
70 |
71 | 72 | 73 |

Added AppStream metainfo.

74 |
75 |
76 | 77 | 78 |

Updated configuration files loading sequence.

79 |
80 |
81 | 82 | 83 |

Updated to follow Freedesktop specifications.

84 |
85 |
86 | 87 | 88 |

Dropped CMake support: I am unable to maintain two different build systems, Meson works better for myself.

89 |

Updated to use Meson options instead of hard-coded bin dir: You can now use Meson's --bindir option to custom binary install path.

90 |
91 |
92 |
93 |
94 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | # Clang Format config taken from Linux Kernel source tree. 2 | # Edited by AlynxZhou. 3 | # Licensed with GPL-2.0. 4 | # Need clang-format v7.0.0 or higher. 5 | --- 6 | AccessModifierOffset: -4 7 | AlignAfterOpenBracket: Align 8 | AlignConsecutiveAssignments: false 9 | AlignConsecutiveDeclarations: false 10 | AlignEscapedNewlines: Left 11 | AlignOperands: true 12 | AlignTrailingComments: false 13 | AllowAllParametersOfDeclarationOnNextLine: false 14 | AllowShortBlocksOnASingleLine: false 15 | AllowShortCaseLabelsOnASingleLine: false 16 | AllowShortFunctionsOnASingleLine: None 17 | AllowShortIfStatementsOnASingleLine: false 18 | AllowShortLoopsOnASingleLine: false 19 | AlwaysBreakAfterDefinitionReturnType: None 20 | AlwaysBreakAfterReturnType: None 21 | AlwaysBreakBeforeMultilineStrings: false 22 | AlwaysBreakTemplateDeclarations: false 23 | BinPackArguments: true 24 | BinPackParameters: true 25 | BraceWrapping: 26 | AfterClass: false 27 | AfterControlStatement: false 28 | AfterEnum: false 29 | AfterFunction: true 30 | AfterNamespace: true 31 | AfterObjCDeclaration: false 32 | AfterStruct: false 33 | AfterUnion: false 34 | AfterExternBlock: false 35 | BeforeCatch: false 36 | BeforeElse: false 37 | IndentBraces: false 38 | SplitEmptyFunction: true 39 | SplitEmptyRecord: true 40 | SplitEmptyNamespace: true 41 | BreakBeforeBinaryOperators: None 42 | BreakBeforeBraces: Custom 43 | BreakBeforeInheritanceComma: false 44 | BreakBeforeTernaryOperators: false 45 | BreakConstructorInitializersBeforeComma: false 46 | BreakConstructorInitializers: BeforeComma 47 | BreakAfterJavaFieldAnnotations: false 48 | BreakStringLiterals: false 49 | ColumnLimit: 80 50 | CommentPragmas: '^ IWYU pragma:' 51 | CompactNamespaces: false 52 | ConstructorInitializerAllOnOneLineOrOnePerLine: false 53 | ConstructorInitializerIndentWidth: 8 54 | ContinuationIndentWidth: 8 55 | Cpp11BracedListStyle: false 56 | DerivePointerAlignment: false 57 | DisableFormat: false 58 | ExperimentalAutoDetectBinPacking: false 59 | FixNamespaceComments: false 60 | 61 | ForEachMacros: 62 | 63 | IncludeBlocks: Preserve 64 | IncludeCategories: 65 | - Regex: '.*' 66 | Priority: 1 67 | IncludeIsMainRegex: '(Test)?$' 68 | IndentCaseLabels: false 69 | IndentPPDirectives: AfterHash 70 | IndentWidth: 8 71 | IndentWrappedFunctionNames: false 72 | JavaScriptQuotes: Leave 73 | JavaScriptWrapImports: true 74 | KeepEmptyLinesAtTheStartOfBlocks: false 75 | MacroBlockBegin: '' 76 | MacroBlockEnd: '' 77 | MaxEmptyLinesToKeep: 1 78 | NamespaceIndentation: Inner 79 | ObjCBinPackProtocolList: Auto 80 | ObjCBlockIndentWidth: 8 81 | ObjCSpaceAfterProperty: true 82 | ObjCSpaceBeforeProtocolList: true 83 | 84 | # Taken from git's rules 85 | PenaltyBreakAssignment: 10 86 | PenaltyBreakBeforeFirstCallParameter: 30 87 | PenaltyBreakComment: 10 88 | PenaltyBreakFirstLessLess: 0 89 | PenaltyBreakString: 10 90 | PenaltyExcessCharacter: 100 91 | PenaltyReturnTypeOnItsOwnLine: 60 92 | 93 | PointerAlignment: Right 94 | ReflowComments: false 95 | SortIncludes: false 96 | SortUsingDeclarations: false 97 | SpaceAfterCStyleCast: false 98 | SpaceAfterTemplateKeyword: true 99 | SpaceBeforeAssignmentOperators: true 100 | SpaceBeforeCtorInitializerColon: true 101 | SpaceBeforeInheritanceColon: true 102 | SpaceBeforeParens: ControlStatements 103 | SpaceBeforeRangeBasedForLoopColon: true 104 | SpaceInEmptyParentheses: false 105 | SpacesBeforeTrailingComments: 1 106 | SpacesInAngles: false 107 | SpacesInContainerLiterals: false 108 | SpacesInCStyleCastParentheses: false 109 | SpacesInParentheses: false 110 | SpacesInSquareBrackets: false 111 | Standard: Cpp03 112 | TabWidth: 8 113 | UseTab: Always 114 | ... 115 | -------------------------------------------------------------------------------- /srcs/flipclock.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Author: Alynx Zhou (https://alynx.one/) 3 | */ 4 | #ifndef __FLIPCLOCK_H__ 5 | #define __FLIPCLOCK_H__ 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #if defined(_WIN32) 13 | # include 14 | #endif 15 | 16 | // Android APP does not generate `config.h` and use its own logger. 17 | #if defined(__ANDROID__) 18 | # include 19 | # define LOG_TAG "FlipClock" 20 | # if defined(__DEBUG__) 21 | # define LOG_DEBUG(...) \ 22 | __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, \ 23 | __VA_ARGS__) 24 | # else 25 | # define LOG_DEBUG(...) 26 | # endif 27 | # define LOG_ERROR(...) \ 28 | __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) 29 | #else 30 | # include 31 | # if defined(__DEBUG__) 32 | # define LOG_DEBUG(...) fprintf(stdout, __VA_ARGS__) 33 | # else 34 | # define LOG_DEBUG(...) 35 | # endif 36 | # define LOG_ERROR(...) fprintf(stderr, __VA_ARGS__) 37 | # include "config.h" 38 | #endif 39 | 40 | /** 41 | * Similiar with GLib. Those macros are only used for debug, which means a 42 | * failure should be a programmer error so the code should be checked. 43 | */ 44 | #define RETURN_IF_FAIL(EXPR) \ 45 | do { \ 46 | if (!(EXPR)) { \ 47 | LOG_ERROR("%s: `%s` failed!\n", __func__, #EXPR); \ 48 | return; \ 49 | } \ 50 | } while (0) 51 | 52 | #define RETURN_VAL_IF_FAIL(EXPR, VAL) \ 53 | do { \ 54 | if (!(EXPR)) { \ 55 | LOG_ERROR("%s: `%s` failed!\n", __func__, #EXPR); \ 56 | return (VAL); \ 57 | } \ 58 | } while (0) 59 | 60 | #define PROGRAM_TITLE "FlipClock" 61 | #define MAX_BUFFER_LENGTH 2048 62 | 63 | struct flipclock { 64 | // Structures not shared by clocks. 65 | struct flipclock_clock **clocks; 66 | // Number of clocks. 67 | int clocks_length; 68 | // Structures shared by clocks. 69 | struct tm now; 70 | SDL_Color box_color; 71 | SDL_Color text_color; 72 | SDL_Color background_color; 73 | char font_path[MAX_BUFFER_LENGTH]; 74 | char conf_path[MAX_BUFFER_LENGTH]; 75 | double text_scale; 76 | double card_scale; 77 | #if defined(_WIN32) 78 | HWND preview_window; 79 | bool preview; 80 | bool screensaver; 81 | char program_dir[MAX_BUFFER_LENGTH]; 82 | #endif 83 | bool ampm; 84 | bool full; 85 | bool show_second; 86 | long long last_touch_time; 87 | SDL_FingerID last_touch_finger; 88 | bool running; 89 | }; 90 | 91 | struct flipclock *flipclock_create(void); 92 | void flipclock_load_conf(struct flipclock *app); 93 | void flipclock_create_clocks(struct flipclock *app); 94 | void flipclock_refresh(struct flipclock *app, int clock_index); 95 | void flipclock_create_textures(struct flipclock *app, int clock_index); 96 | void flipclock_destroy_textures(struct flipclock *app, int clock_index); 97 | void flipclock_open_fonts(struct flipclock *app, int clock_index); 98 | void flipclock_close_fonts(struct flipclock *app, int clock_index); 99 | void flipclock_run_mainloop(struct flipclock *app); 100 | void flipclock_destroy_clocks(struct flipclock *app); 101 | void flipclock_destroy(struct flipclock *app); 102 | void flipclock_print_help(struct flipclock *app, char program_name[]); 103 | 104 | #endif 105 | -------------------------------------------------------------------------------- /srcs/main.c: -------------------------------------------------------------------------------- 1 | /** 2 | * Alynx Zhou (https://alynx.one/) 3 | */ 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | #include "getarg.h" 12 | #include "flipclock.h" 13 | 14 | int main(int argc, char *argv[]) 15 | { 16 | if (SDL_Init(SDL_INIT_VIDEO) < 0) { 17 | LOG_ERROR("%s\n", SDL_GetError()); 18 | exit(EXIT_FAILURE); 19 | } 20 | if (TTF_Init() < 0) { 21 | LOG_ERROR("%s\n", TTF_GetError()); 22 | exit(EXIT_FAILURE); 23 | } 24 | SDL_SetHint(SDL_HINT_VIDEO_MINIMIZE_ON_FOCUS_LOSS, "0"); 25 | struct flipclock *app = flipclock_create(); 26 | #if !defined(__ANDROID__) 27 | // Android don't need conf and arguments. 28 | flipclock_load_conf(app); 29 | # if defined(__DEBUG__) 30 | for (int i = 0; i < argc; ++i) 31 | LOG_DEBUG("argv[%d]: %s\n", i, argv[i]); 32 | # endif 33 | # if defined(_WIN32) 34 | char OPT_STRING[] = "hvscp:3wt:f:"; 35 | # else 36 | char OPT_STRING[] = "hv3wt:f:"; 37 | # endif 38 | int opt = 0; 39 | bool exit_after_argument = false; 40 | while ((opt = getarg(argc, argv, OPT_STRING)) != -1) { 41 | switch (opt) { 42 | case 'h': 43 | flipclock_print_help(app, argv[0]); 44 | exit_after_argument = true; 45 | break; 46 | case 'v': 47 | printf(PROJECT_VERSION "\n"); 48 | exit_after_argument = true; 49 | break; 50 | # if defined(_WIN32) 51 | // See . 52 | case 's': 53 | /** 54 | * One of the most silly requirement I've seen. 55 | * But it seems I can use it to handle key press. 56 | */ 57 | app->screensaver = true; 58 | /** 59 | * Even if user set windowed mode in configuration file, 60 | * screensaver still needs to be fullscreen. 61 | */ 62 | app->full = true; 63 | break; 64 | case 'c': 65 | MessageBox(NULL, 66 | "Please read and edit flipclock.conf " 67 | "under program directory to configure it!", 68 | PROGRAM_TITLE, MB_OK); 69 | exit_after_argument = true; 70 | break; 71 | case 'p': 72 | app->preview = true; 73 | if (argopt == NULL) { 74 | LOG_ERROR("Missing value for option `%c%c`\n", 75 | OPT_START, opt); 76 | exit_after_argument = true; 77 | break; 78 | } 79 | /** 80 | * See . 81 | * typedef void *PVOID; 82 | * typedef PVOID HANDLE; 83 | * typedef HANDLE HWND; 84 | * So it's safe to treat it as a unsigned int, even 85 | * though silly MSVC will complain. If MSVC is unhappy, 86 | * why not tell us how to handle it correctly? 87 | * Seems Windows print HWND as a decimal number, 88 | * so %p with scanf() is not suitable here. 89 | */ 90 | app->preview_window = strtoul(argopt, NULL, 0); 91 | break; 92 | # endif 93 | /** 94 | * Windows taken `/s` for screensaver, and if you run `.scr` 95 | * file from explorer, it will add `/S`! What a silly behavior, 96 | * so we cannot use those chars. 97 | */ 98 | case '3': 99 | app->show_second = true; 100 | break; 101 | case 'w': 102 | app->full = false; 103 | break; 104 | case 't': 105 | if (argopt == NULL) { 106 | LOG_ERROR("Missing value for option `%c%c`\n", 107 | OPT_START, opt); 108 | exit_after_argument = true; 109 | break; 110 | } 111 | if (atoi(argopt) == 12) 112 | app->ampm = true; 113 | else 114 | app->ampm = false; 115 | break; 116 | case 'f': 117 | if (argopt == NULL) { 118 | LOG_ERROR("Missing value for option `%c%c`\n", 119 | OPT_START, opt); 120 | exit_after_argument = true; 121 | break; 122 | } 123 | strncpy(app->font_path, argopt, MAX_BUFFER_LENGTH); 124 | app->font_path[MAX_BUFFER_LENGTH - 1] = '\0'; 125 | if (strlen(app->font_path) == MAX_BUFFER_LENGTH - 1) 126 | LOG_ERROR("`font_path` too long, " 127 | "may fail to load.\n"); 128 | break; 129 | case 0: 130 | LOG_ERROR("%s: Invalid value `%s`.\n", argv[0], argopt); 131 | break; 132 | default: 133 | LOG_ERROR("%s: Invalid option `%c%c`.\n", argv[0], 134 | OPT_START, opt); 135 | break; 136 | } 137 | } 138 | if (exit_after_argument) 139 | goto exit; 140 | #endif 141 | 142 | flipclock_create_clocks(app); 143 | 144 | flipclock_run_mainloop(app); 145 | 146 | flipclock_destroy_clocks(app); 147 | 148 | exit: 149 | flipclock_destroy(app); 150 | TTF_Quit(); 151 | SDL_Quit(); 152 | return 0; 153 | } 154 | -------------------------------------------------------------------------------- /docs/privacy-policy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | FlipClock Privacy Policy 13 | 14 | 15 |

Privacy Policy

16 |

Alynx Zhou built the FlipClock app as a open source app. This SERVICE is provided by Alynx Zhou at no cost and is intended 17 | for use as is.

18 |

This page is used to inform website visitors regarding my policies with the collection, use, and 19 | disclosure of Personal Information if anyone decided to use my Service.

20 |

If you choose to use my Service, then you agree to the collection and use of information in 21 | relation with this policy. The Personal Information that I collect are used for providing and 22 | improving the Service. I will not use or share your information with anyone except as described 23 | in this Privacy Policy.

24 |

The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, 25 | which is accessible at FlipClock, unless otherwise defined in this Privacy Policy.

26 | 27 |

Information Collection and Use

28 |

I require you to provide us with no 29 | personally identifiable information. 30 | The information that I request is retained on your device and is not 31 | collected by me in any way.

32 |

The app does use third party services that may collect information used to identify you.

33 | 34 |

Log Data

35 |

I want to inform you that whenever you use my Service, in case of an error in the app I collect 36 | data and information (through third party products) on your phone called Log Data. This Log Data 37 | may include information such as your devices’s Internet Protocol (“IP”) address, device name, 38 | operating system version, configuration of the app when utilising my Service, the time and date 39 | of your use of the Service, and other statistics.

40 | 41 |

Cookies

42 |

Cookies are files with small amount of data that is commonly used an anonymous unique identifier. 43 | These are sent to your browser from the website that you visit and are stored on your devices’s 44 | internal memory.

45 |

This Services does not uses these “cookies”.

46 | 47 |

Security

48 |

I value your trust in providing us your Personal Information, thus we are striving to use 49 | commercially acceptable means of protecting it. But remember that no method of transmission over 50 | the internet, or method of electronic storage is 100% secure and reliable, and I cannot 51 | guarantee its absolute security.

52 | 53 |

Links to Other Sites

54 |

This Service may contain links to other sites. If you click on a third-party link, you will be 55 | directed to that site. Note that these external sites are not operated by me. Therefore, I 56 | strongly advise you to review the Privacy Policy of these websites. I have no control over, and 57 | assume no responsibility for the content, privacy policies, or practices of any third-party 58 | sites or services.

59 | 60 |

Children’s Privacy

61 |

This Services do not address anyone under the age of 13. I do not knowingly collect personal 62 | identifiable information from children under 13. In the case I discover that a child under 13 63 | has provided me with personal information, I immediately delete this from our servers. If you 64 | are a parent or guardian and you are aware that your child has provided us with personal 65 | information, please contact me so that I will be able to do necessary actions.

66 | 67 |

Changes to This Privacy Policy

68 |

I may update our Privacy Policy from time to time. Thus, you are advised to review this page 69 | periodically for any changes. I will notify you of any changes by posting the new Privacy Policy 70 | on this page. These changes are effective immediately, after they are posted on this page.

71 | 72 |

Contact Us

73 |

If you have any questions or suggestions about my Privacy Policy, do not hesitate to contact 74 | me.

75 | 76 | 77 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | # If we want to build static program with wrap on Windows, 2 | # We need to set `default_library=static` here 3 | # instead of setting it for each dependencies. 4 | # Because though FlipClock does not link against freetype2 directly, 5 | # sdl2_ttf needs to link against freetype2, 6 | # and if we only pass `default_library=static` to sdl2_ttf, 7 | # it still links against freetype2 dynamically. 8 | project( 9 | 'flipclock', 10 | 'c', 11 | version: '2.10.0', 12 | license: 'Apache-2.0', 13 | default_options: ['c_std=c11', 'default_library=static'] 14 | ) 15 | 16 | cc = meson.get_compiler('c') 17 | 18 | c_args = [] 19 | if get_option('debug') == true 20 | warning_level = 3 21 | c_args += ['-D__DEBUG__'] 22 | endif 23 | 24 | conf_data = configuration_data() 25 | conf_data.set('project_version', meson.project_version()) 26 | conf_data.set('package_bindir', get_option('prefix') / get_option('bindir')) 27 | conf_data.set('package_datadir', get_option('prefix') / get_option('datadir')) 28 | # `sysconfdir` is `/etc` if `prefix` is `/usr`, 29 | # but it will be relative path if using other prefix. 30 | # See . 31 | # Meson's string path building operator will not join two absolute path, 32 | # it will keep the right one, so it's OK to handle `sysconfdir` with it. 33 | # See . 34 | conf_data.set('package_sysconfdir', get_option('prefix') / get_option('sysconfdir')) 35 | 36 | configure_file( 37 | input: 'srcs/config.h.in', 38 | output: 'config.h', 39 | configuration: conf_data 40 | ) 41 | 42 | # Don't install desktop entry for Windows. 43 | if host_machine.system() == 'linux' 44 | configure_file( 45 | input: 'dists/one.alynx.FlipClock.desktop.in', 46 | output: 'one.alynx.FlipClock.desktop', 47 | configuration: conf_data, 48 | install: true, 49 | # If a relative path is given, meson will put it under prefix 50 | # so we don't need to put `get_option('prefix')` manually here. 51 | install_dir: get_option('datadir') / 'applications' 52 | ) 53 | endif 54 | 55 | sources = files( 56 | 'srcs/main.c', 57 | 'srcs/getarg.c', 58 | 'srcs/card.c', 59 | 'srcs/clock.c', 60 | 'srcs/flipclock.c' 61 | ) 62 | 63 | dependencies = [] 64 | include_directories = [] 65 | # Windows does not have sperated libm. 66 | if host_machine.system() == 'linux' 67 | m = cc.find_library('m', required: true) 68 | dependencies += [m] 69 | endif 70 | # Wrap files will be used as fallback. 71 | sdl2 = dependency('sdl2', required: true) 72 | sdl2_ttf = dependency('SDL2_ttf', required: true) 73 | dependencies += [sdl2, sdl2_ttf] 74 | 75 | if host_machine.system() == 'linux' or host_machine.system() == 'darwin' 76 | executable( 77 | 'flipclock', 78 | sources: sources, 79 | c_args: c_args, 80 | dependencies: dependencies, 81 | include_directories: include_directories, 82 | install: true 83 | # By default, meson install binary to 84 | # `get_option('prefix') / get_option('bindir')`, 85 | # so we can omit `install_dir` here. 86 | ) 87 | 88 | install_data( 89 | 'dists' / 'flipclock.conf', 90 | install_dir: get_option('sysconfdir') 91 | ) 92 | install_data( 93 | 'dists' / 'flipclock.ttf', 94 | install_dir: get_option('datadir') / 'fonts' 95 | ) 96 | install_data( 97 | 'dists' / 'icons' / '128x128' / 'one.alynx.FlipClock.png', 98 | install_dir: get_option('datadir') / 'icons' / 'hicolor' / '128x128' / 'apps' 99 | ) 100 | install_data( 101 | 'dists' / 'icons' / '64x64' / 'one.alynx.FlipClock.png', 102 | install_dir: get_option('datadir') / 'icons' / 'hicolor' / '64x64' / 'apps' 103 | ) 104 | install_data( 105 | 'dists' / 'icons' / 'scalable' / 'one.alynx.FlipClock.svg', 106 | install_dir: get_option('datadir') / 'icons' / 'hicolor' / 'scalable' / 'apps' 107 | ) 108 | install_data( 109 | 'dists' / 'one.alynx.FlipClock.metainfo.xml', 110 | install_dir: get_option('datadir') / 'metainfo' 111 | ) 112 | install_data( 113 | 'LICENSE', 114 | install_dir: get_option('datadir') / 'licenses' / meson.project_name() 115 | ) 116 | elif host_machine.system() == 'windows' 117 | if get_option('debug') == true 118 | executable( 119 | 'flipclock', 120 | sources: sources, 121 | c_args: c_args, 122 | dependencies: dependencies, 123 | include_directories: include_directories, 124 | install: true, 125 | install_dir: meson.project_name(), 126 | name_suffix: 'scr', 127 | win_subsystem: 'console' 128 | ) 129 | else 130 | executable( 131 | 'flipclock', 132 | sources: sources, 133 | c_args: c_args, 134 | dependencies: dependencies, 135 | include_directories: include_directories, 136 | install: true, 137 | install_dir: meson.project_name(), 138 | name_suffix: 'scr', 139 | win_subsystem: 'windows' 140 | ) 141 | endif 142 | 143 | install_data( 144 | 'dists' / 'flipclock.conf', 145 | install_dir: meson.project_name() 146 | ) 147 | install_data( 148 | 'dists' / 'flipclock.ttf', 149 | install_dir: meson.project_name() 150 | ) 151 | install_data( 152 | 'LICENSE', 153 | install_dir: meson.project_name() 154 | ) 155 | install_data( 156 | 'dists' / 'COPYING', 157 | install_dir: meson.project_name() 158 | ) 159 | install_data( 160 | 'dists' / '请先读我.txt', 161 | install_dir: meson.project_name() 162 | ) 163 | endif 164 | -------------------------------------------------------------------------------- /dists/icons/scalable/one.alynx.FlipClock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 35 | 37 | 43 | 49 | 55 | 61 | 67 | 68 | 72 | 80 | 84 | 88 | 92 | 96 | 1 107 | 1 118 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | FlipClock 2 | ========= 3 | 4 | A flip clock screensaver supported by SDL2. 5 | ------------------------------------------- 6 | 7 | [Project Website](https://flipclock.alynx.one) 8 | 9 | ![Screenshot](screenshot.png) 10 | 11 | # Installation 12 | 13 | ## Distribution Package (Recommended) 14 | 15 | ### Arch Linux 16 | 17 | #### Install From AUR 18 | 19 | You can get PKGBUILD from [FlipClock's AUR page](https://aur.archlinux.org/packages/flipclock/) and build it manually, or use some AUR helpers. 20 | 21 | ``` 22 | $ paru flipclock 23 | ``` 24 | 25 | #### Install From `archlinuxcn` Repo 26 | 27 | First [add `archlinuxcn` repo to your system](https://www.archlinuxcn.org/archlinux-cn-repo-and-mirror/), then use `pacman` to install it. 28 | 29 | ``` 30 | # pacman -S flipclock 31 | ``` 32 | 33 | ### Flatpak 34 | 35 | FlipClock is also available as Flatpak on Flathub, you can get it from [FlipClock's Flathub page](https://flathub.org/apps/details/one.alynx.FlipClock), or using `flatpak` CLI tool. 36 | 37 | ``` 38 | # flatpak install flathub one.alynx.FlipClock && flatpak run one.alynx.FlipClock 39 | ``` 40 | 41 | ### Other Linux Distributions 42 | 43 | Please help package FlipClock to your distribution! 44 | 45 | ### Windows 46 | 47 | Just download file with `win` in its name from [release page](https://github.com/AlynxZhou/flipclock/releases/), extract it and right click `flipclock.scr` to install it as a screensaver. Please note I may not have time to build every version for Windows, just pick the latest available one. 48 | 49 | ## Build From Source 50 | 51 | ### Linux 52 | 53 | 1. Install a C compiler, Meson, Ninja, libc, libm, SDL2 and SDL2_ttf. 54 | 2. `mkdir build && cd build && meson setup . .. && meson compile` 55 | 3. `./flipclock -f ../dists/flipclock.ttf` 56 | 4. If you want to install this to your system, you could use `mkdir build && cd build && meson setup --prefix=/usr --buildtype=release . .. && meson compile && sudo meson install`. 57 | 58 | ### Windows 59 | 60 | I saw a windows user says "This program has dlls in its folder so it's not simple!" and I got angry. Anyone who knows compiling, linking and loading won't complain. It might be quite hard for some Windows users to understand how hard to build static libraries. Windows is a horrible platform for developers: there is no package manager so you have to package all dependencies, Visual Studio is slow and complex on adding dependenvies. Thanks to Meson which handles all dirty things, its SDL2 wrap works good and I managed to tweak it to build a static linked program automatically if no pre-built dependency found. 61 | 62 | 1. Install Meson, Ninja, Visual Studio. 63 | 2. Create a prefix directory, for example `d:/flipclock-prefix`, program files will be installed into it. 64 | 3. Open `x64 Native Tools Command Prompt for VS 2019` from Start Menu, or other architectures you need. 65 | 4. Change dir to where you put this project. Run `mkdir build && cd build && meson setup --prefix=d:/flipclock-prefix --buildtype=release . .. && meson compile && meson install`. You can change prefix argument to other path you created in Step 2, but you need to use UNIX style slash instead of backslash because it's escape character in C. 66 | 5. Go to `flipclock` dir under your prefix directory, you can now find `flipclock.scr` and right click it to install it as a screensaver. 67 | 68 | ### Android 69 | 70 | See [flipclock-android](https://github.com/AlynxZhou/flipclock-android/). It may be obsolete because I don't have enough time to update the Android wrapper. 71 | 72 | # Configuration 73 | 74 | On Linux, program will first use `$XDG_CONFIG_HOME/flipclock.conf`, if `XDG_CONFIG_HOME` is not set or file does not exist, it will use `$HOME/.config/flipclock.conf`. If per-user configuration file does not exist, it will use `/etc/flipclock.conf` or `flipclock.conf` under `sysconfdir` you choosed while building. 75 | 76 | If you want to run this program under Wayland, you can set environment variable `SDL_VIDEODRIVER` to `wayland`: 77 | 78 | ``` 79 | SDL_VIDEODRIVER=wayland 80 | ``` 81 | 82 | On Windows, program will use `flipclock.conf` under the same directory as program. 83 | 84 | `flipclock.conf` should be installed with the binary by Meson. 85 | 86 | # About Fullscreen and Multi-Monitor 87 | 88 | This program has multi-monitor support when it is started as fullscreen, but adding/removing monitors while program is running is not supported, and you should not do this. 89 | 90 | If you run this program in windowed mode (`-w`), it will only create one window. 91 | 92 | # Contribution 93 | 94 | If you want some features and you can implement it, PRs are always welcome, but there are some rules or personal habits: 95 | 96 | - If you are writing multi-line comment, please use the same style with existing comments. Comments should always occupy a new line. If your comment is longer than Column 80, break it into block comment with `/* */` (but don't break long URL, it's fine), don't use `//` for block comment. 97 | - You can use all **C11** features freely. 98 | - Try to use C standard functions first, until you are implementing some platform-dependent features that libc does not support. Do use preprocessor (`_WIN32`, `__ANDROID__` and `__linux__`) for platform-dependent code. (Note: Android will also define `__linux__`, so if you are targeting traditional Linux distributions, please use `defined(__linux__) && !defined(__ANDROID__)`.) 99 | - Use `RETURN_IF_FAIL(EXPR)` or `RETURN_VAL_IF_FAIL(EXPR, VAL)` from `flipclock.h` to check arguments for newly added functions. 100 | - Try not to pull new dependencies into project other than SDL2 and SDL2_ttf, it's too brain damage to add dependencies when building on Windows, building on this platform is a disaster and packaging on this platform is a mistery. 101 | - **When you are coding please use [Linux kernel coding style](https://www.kernel.org/doc/html/latest/process/coding-style.html).** There is a `.clang-format` for this project, please run `clang-format` yourself before committing something. It will keep most coding style consistent. 102 | - There are still some coding style `clang-format` cannot change, please keep the same as existing code. For example, add period for all comments and printed text, add `\n` yourself for logging. 103 | - Prefer to `++i`, except when you really need `i` before increasement. 104 | - I prefer to write commit message in past tense, capitalize the first character and add period. For example "Added new feature.", "Updated README.md.". 105 | - If you added new options to configuration file and you are able to write Chinese, please also update `dists/请先读我.txt`. This file is a README for Chinese Windows users and **should use GB2312 as encoding and CRLF as return**. 106 | 107 | # License 108 | 109 | [Apache-2.0](./LICENSE) 110 | -------------------------------------------------------------------------------- /docs/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | text-decoration: underline dotted; /* 2 */ 89 | } 90 | 91 | /** 92 | * Add the correct font weight in Chrome, Edge, and Safari. 93 | */ 94 | 95 | b, 96 | strong { 97 | font-weight: bolder; 98 | } 99 | 100 | /** 101 | * 1. Correct the inheritance and scaling of font size in all browsers. 102 | * 2. Correct the odd `em` font sizing in all browsers. 103 | */ 104 | 105 | code, 106 | kbd, 107 | samp { 108 | font-family: monospace, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in 122 | * all browsers. 123 | */ 124 | 125 | sub, 126 | sup { 127 | font-size: 75%; 128 | line-height: 0; 129 | position: relative; 130 | vertical-align: baseline; 131 | } 132 | 133 | sub { 134 | bottom: -0.25em; 135 | } 136 | 137 | sup { 138 | top: -0.5em; 139 | } 140 | 141 | /* Embedded content 142 | ========================================================================== */ 143 | 144 | /** 145 | * Remove the border on images inside links in IE 10. 146 | */ 147 | 148 | img { 149 | border-style: none; 150 | } 151 | 152 | /* Forms 153 | ========================================================================== */ 154 | 155 | /** 156 | * 1. Change the font styles in all browsers. 157 | * 2. Remove the margin in Firefox and Safari. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: 1.15; /* 1 */ 168 | margin: 0; /* 2 */ 169 | } 170 | 171 | /** 172 | * Show the overflow in IE. 173 | * 1. Show the overflow in Edge. 174 | */ 175 | 176 | button, 177 | input { /* 1 */ 178 | overflow: visible; 179 | } 180 | 181 | /** 182 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 183 | * 1. Remove the inheritance of text transform in Firefox. 184 | */ 185 | 186 | button, 187 | select { /* 1 */ 188 | text-transform: none; 189 | } 190 | 191 | /** 192 | * Correct the inability to style clickable types in iOS and Safari. 193 | */ 194 | 195 | button, 196 | [type="button"], 197 | [type="reset"], 198 | [type="submit"] { 199 | -webkit-appearance: button; 200 | } 201 | 202 | /** 203 | * Remove the inner border and padding in Firefox. 204 | */ 205 | 206 | button::-moz-focus-inner, 207 | [type="button"]::-moz-focus-inner, 208 | [type="reset"]::-moz-focus-inner, 209 | [type="submit"]::-moz-focus-inner { 210 | border-style: none; 211 | padding: 0; 212 | } 213 | 214 | /** 215 | * Restore the focus styles unset by the previous rule. 216 | */ 217 | 218 | button:-moz-focusring, 219 | [type="button"]:-moz-focusring, 220 | [type="reset"]:-moz-focusring, 221 | [type="submit"]:-moz-focusring { 222 | outline: 1px dotted ButtonText; 223 | } 224 | 225 | /** 226 | * Correct the padding in Firefox. 227 | */ 228 | 229 | fieldset { 230 | padding: 0.35em 0.75em 0.625em; 231 | } 232 | 233 | /** 234 | * 1. Correct the text wrapping in Edge and IE. 235 | * 2. Correct the color inheritance from `fieldset` elements in IE. 236 | * 3. Remove the padding so developers are not caught out when they zero out 237 | * `fieldset` elements in all browsers. 238 | */ 239 | 240 | legend { 241 | box-sizing: border-box; /* 1 */ 242 | color: inherit; /* 2 */ 243 | display: table; /* 1 */ 244 | max-width: 100%; /* 1 */ 245 | padding: 0; /* 3 */ 246 | white-space: normal; /* 1 */ 247 | } 248 | 249 | /** 250 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 251 | */ 252 | 253 | progress { 254 | vertical-align: baseline; 255 | } 256 | 257 | /** 258 | * Remove the default vertical scrollbar in IE 10+. 259 | */ 260 | 261 | textarea { 262 | overflow: auto; 263 | } 264 | 265 | /** 266 | * 1. Add the correct box sizing in IE 10. 267 | * 2. Remove the padding in IE 10. 268 | */ 269 | 270 | [type="checkbox"], 271 | [type="radio"] { 272 | box-sizing: border-box; /* 1 */ 273 | padding: 0; /* 2 */ 274 | } 275 | 276 | /** 277 | * Correct the cursor style of increment and decrement buttons in Chrome. 278 | */ 279 | 280 | [type="number"]::-webkit-inner-spin-button, 281 | [type="number"]::-webkit-outer-spin-button { 282 | height: auto; 283 | } 284 | 285 | /** 286 | * 1. Correct the odd appearance in Chrome and Safari. 287 | * 2. Correct the outline style in Safari. 288 | */ 289 | 290 | [type="search"] { 291 | -webkit-appearance: textfield; /* 1 */ 292 | outline-offset: -2px; /* 2 */ 293 | } 294 | 295 | /** 296 | * Remove the inner padding in Chrome and Safari on macOS. 297 | */ 298 | 299 | [type="search"]::-webkit-search-decoration { 300 | -webkit-appearance: none; 301 | } 302 | 303 | /** 304 | * 1. Correct the inability to style clickable types in iOS and Safari. 305 | * 2. Change font properties to `inherit` in Safari. 306 | */ 307 | 308 | ::-webkit-file-upload-button { 309 | -webkit-appearance: button; /* 1 */ 310 | font: inherit; /* 2 */ 311 | } 312 | 313 | /* Interactive 314 | ========================================================================== */ 315 | 316 | /* 317 | * Add the correct display in Edge, IE 10+, and Firefox. 318 | */ 319 | 320 | details { 321 | display: block; 322 | } 323 | 324 | /* 325 | * Add the correct display in all browsers. 326 | */ 327 | 328 | summary { 329 | display: list-item; 330 | } 331 | 332 | /* Misc 333 | ========================================================================== */ 334 | 335 | /** 336 | * Add the correct display in IE 10+. 337 | */ 338 | 339 | template { 340 | display: none; 341 | } 342 | 343 | /** 344 | * Add the correct display in IE 10. 345 | */ 346 | 347 | [hidden] { 348 | display: none; 349 | } 350 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /srcs/clock.c: -------------------------------------------------------------------------------- 1 | #include "srcs/card.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include "flipclock.h" 9 | #include "clock.h" 10 | #include "card.h" 11 | 12 | #define WINDOW_WIDTH 800 13 | #define WINDOW_HEIGHT 600 14 | 15 | static void _flipclock_clock_update_layout(struct flipclock_clock *clock) 16 | { 17 | RETURN_IF_FAIL(clock != NULL); 18 | 19 | const struct flipclock *app = clock->app; 20 | SDL_Rect hour_rect; 21 | SDL_Rect minute_rect; 22 | SDL_Rect second_rect; 23 | int cards_length = app->show_second ? 3 : 2; 24 | int spaces_length = cards_length + 1; 25 | // space/card = 1/8. 26 | /** 27 | * In best condition, we have 1 + 8 + 1 + 8 + 1. However, the other 28 | * length of window might be smaller, and the card is less than 8. We 29 | * will enlarge the spaces of begining and end, so only care about 30 | * spaces between cards when calculating position. 31 | */ 32 | if (clock->w >= clock->h) { 33 | int space_size = clock->w / (cards_length * 8 + spaces_length); 34 | int min_height = clock->h * 0.8; 35 | int min_width = 36 | clock->w * 8 / (cards_length * 8 + spaces_length); 37 | int card_size = min_height < min_width ? min_height : min_width; 38 | card_size *= app->card_scale; 39 | 40 | hour_rect.x = (clock->w - card_size * cards_length - 41 | space_size * (spaces_length - 2)) / 42 | 2; 43 | hour_rect.y = (clock->h - card_size) / 2; 44 | hour_rect.w = card_size; 45 | hour_rect.h = card_size; 46 | flipclock_card_set_rect(clock->hour, hour_rect); 47 | 48 | minute_rect.x = hour_rect.x + hour_rect.w + space_size; 49 | minute_rect.y = hour_rect.y; 50 | minute_rect.w = card_size; 51 | minute_rect.h = card_size; 52 | flipclock_card_set_rect(clock->minute, minute_rect); 53 | 54 | if (app->show_second) { 55 | second_rect.x = hour_rect.x + hour_rect.w + space_size + 56 | minute_rect.w + space_size; 57 | second_rect.y = hour_rect.y; 58 | second_rect.w = card_size; 59 | second_rect.h = card_size; 60 | flipclock_card_set_rect(clock->second, second_rect); 61 | } 62 | } else { 63 | int space_size = clock->h / (cards_length * 8 + spaces_length); 64 | int min_width = clock->w * 0.8; 65 | int min_height = 66 | clock->h * 8 / (cards_length * 8 + spaces_length); 67 | int card_size = min_height < min_width ? min_height : min_width; 68 | card_size *= app->card_scale; 69 | 70 | hour_rect.x = (clock->w - card_size) / 2; 71 | hour_rect.y = (clock->h - card_size * cards_length - 72 | space_size * (spaces_length - 2)) / 73 | 2; 74 | hour_rect.w = card_size; 75 | hour_rect.h = card_size; 76 | flipclock_card_set_rect(clock->hour, hour_rect); 77 | 78 | minute_rect.y = hour_rect.y + hour_rect.h + space_size; 79 | minute_rect.x = hour_rect.x; 80 | minute_rect.w = card_size; 81 | minute_rect.h = card_size; 82 | flipclock_card_set_rect(clock->minute, minute_rect); 83 | 84 | if (app->show_second) { 85 | second_rect.y = hour_rect.y + hour_rect.h + space_size + 86 | minute_rect.h + space_size; 87 | second_rect.x = hour_rect.x; 88 | second_rect.w = card_size; 89 | second_rect.h = card_size; 90 | flipclock_card_set_rect(clock->second, second_rect); 91 | } 92 | } 93 | } 94 | 95 | static void _flipclock_clock_create_cards(struct flipclock_clock *clock) 96 | { 97 | RETURN_IF_FAIL(clock != NULL); 98 | 99 | struct flipclock *app = clock->app; 100 | 101 | clock->hour = flipclock_card_create(app, clock->renderer); 102 | clock->minute = flipclock_card_create(app, clock->renderer); 103 | clock->second = NULL; 104 | if (app->show_second) 105 | clock->second = flipclock_card_create(app, clock->renderer); 106 | _flipclock_clock_update_layout(clock); 107 | } 108 | 109 | struct flipclock_clock *flipclock_clock_create(struct flipclock *app, int i) 110 | { 111 | RETURN_VAL_IF_FAIL(app != NULL, NULL); 112 | 113 | /** 114 | * We need `SDL_WINDOW_RESIZABLE` for auto-rotate while fullscreen on 115 | * Android. 116 | * 117 | * Being HiDPI aware aware on macOS requires extra files, we don't have 118 | * it so disable this currently. 119 | */ 120 | unsigned int flags = SDL_WINDOW_SHOWN | 121 | #ifndef __APPLE__ 122 | SDL_WINDOW_ALLOW_HIGHDPI | 123 | #endif 124 | SDL_WINDOW_RESIZABLE; 125 | if (app->full) { 126 | flags = SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE | 127 | #ifndef __APPLE__ 128 | SDL_WINDOW_ALLOW_HIGHDPI | 129 | #endif 130 | SDL_WINDOW_FULLSCREEN_DESKTOP; 131 | } 132 | struct flipclock_clock *clock = malloc(sizeof(*clock)); 133 | if (clock == NULL) { 134 | LOG_ERROR("Failed to create clock!"); 135 | exit(EXIT_FAILURE); 136 | } 137 | clock->app = app; 138 | clock->waiting = false; 139 | clock->i = i; 140 | SDL_Rect display_bounds; 141 | SDL_GetDisplayBounds(i, &display_bounds); 142 | // Give each window a unique title. 143 | char window_title[MAX_BUFFER_LENGTH]; 144 | snprintf(window_title, MAX_BUFFER_LENGTH, PROGRAM_TITLE " %d", i); 145 | if (app->full) 146 | clock->window = SDL_CreateWindow(window_title, display_bounds.x, 147 | display_bounds.y, 148 | display_bounds.w, 149 | display_bounds.h, flags); 150 | else 151 | clock->window = SDL_CreateWindow( 152 | window_title, 153 | display_bounds.x + 154 | (display_bounds.w - WINDOW_WIDTH) / 2, 155 | display_bounds.y + 156 | (display_bounds.h - WINDOW_HEIGHT) / 2, 157 | WINDOW_WIDTH, WINDOW_HEIGHT, flags); 158 | if (clock->window == NULL) { 159 | LOG_ERROR("%s\n", SDL_GetError()); 160 | exit(EXIT_FAILURE); 161 | } 162 | // Get actual window size after create it. 163 | SDL_GetWindowSize(clock->window, &clock->w, &clock->h); 164 | clock->renderer = SDL_CreateRenderer( 165 | clock->window, -1, 166 | SDL_RENDERER_ACCELERATED | SDL_RENDERER_TARGETTEXTURE | 167 | SDL_RENDERER_PRESENTVSYNC); 168 | if (clock->renderer == NULL) { 169 | LOG_ERROR("%s\n", SDL_GetError()); 170 | exit(EXIT_FAILURE); 171 | } 172 | SDL_SetRenderDrawBlendMode(clock->renderer, SDL_BLENDMODE_BLEND); 173 | _flipclock_clock_create_cards(clock); 174 | return clock; 175 | } 176 | 177 | #if defined(_WIN32) 178 | /** 179 | * Create clock from given HWND, which should be a subwindow of screensaver 180 | * preview. 181 | */ 182 | struct flipclock_clock *flipclock_clock_create_preview(struct flipclock *app) 183 | { 184 | RETURN_VAL_IF_FAIL(app != NULL, NULL); 185 | 186 | struct flipclock_clock *clock = malloc(sizeof(*clock)); 187 | if (clock == NULL) { 188 | LOG_ERROR("Failed to create clock!"); 189 | exit(EXIT_FAILURE); 190 | } 191 | clock->app = app; 192 | clock->waiting = false; 193 | clock->i = 0; 194 | clock->window = SDL_CreateWindowFrom(app->preview_window); 195 | if (clock->window == NULL) { 196 | LOG_ERROR("%s\n", SDL_GetError()); 197 | exit(EXIT_FAILURE); 198 | } 199 | // Get actual window size after create it. 200 | SDL_GetWindowSize(clock->window, &clock->w, &clock->h); 201 | clock->renderer = SDL_CreateRenderer( 202 | clock->window, -1, 203 | SDL_RENDERER_ACCELERATED | SDL_RENDERER_TARGETTEXTURE | 204 | SDL_RENDERER_PRESENTVSYNC); 205 | if (clock->renderer == NULL) { 206 | LOG_ERROR("%s\n", SDL_GetError()); 207 | exit(EXIT_FAILURE); 208 | } 209 | SDL_SetRenderDrawBlendMode(clock->renderer, SDL_BLENDMODE_BLEND); 210 | _flipclock_clock_create_cards(clock); 211 | return clock; 212 | } 213 | #endif 214 | 215 | void flipclock_clock_set_show_second(struct flipclock_clock *clock, 216 | bool show_second) 217 | { 218 | RETURN_IF_FAIL(clock != NULL); 219 | 220 | if (show_second) { 221 | if (clock->second == NULL) 222 | clock->second = flipclock_card_create(clock->app, 223 | clock->renderer); 224 | } else { 225 | if (clock->second != NULL) { 226 | flipclock_card_destory(clock->second); 227 | clock->second = NULL; 228 | } 229 | } 230 | // Toggling seconds always changes size. 231 | _flipclock_clock_update_layout(clock); 232 | } 233 | 234 | void flipclock_clock_set_fullscreen(struct flipclock_clock *clock, bool full) 235 | { 236 | RETURN_IF_FAIL(clock != NULL); 237 | 238 | // Let's find which display the clock is inside. 239 | SDL_Rect display_bounds; 240 | int clock_x; 241 | int clock_y; 242 | SDL_GetWindowPosition(clock->window, &clock_x, &clock_y); 243 | int clock_center_x = clock_x + clock->w / 2; 244 | int clock_center_y = clock_y + clock->h / 2; 245 | int displays_length = SDL_GetNumVideoDisplays(); 246 | // If a clock is out of all displays it will be re-placed into the last. 247 | for (int i = 0; i < displays_length; ++i) { 248 | SDL_GetDisplayBounds(i, &display_bounds); 249 | if (clock_center_x >= display_bounds.x && 250 | clock_center_x < display_bounds.x + display_bounds.w && 251 | clock_center_y >= display_bounds.y && 252 | clock_center_y < display_bounds.y + display_bounds.h) { 253 | LOG_DEBUG("Clock `%d` is inside display `%d`.\n", 254 | clock->i, i); 255 | break; 256 | } 257 | } 258 | if (full) { 259 | // Move clocks to their placed displays. 260 | SDL_SetWindowPosition(clock->window, display_bounds.x, 261 | display_bounds.y); 262 | SDL_SetWindowFullscreen(clock->window, 263 | SDL_WINDOW_FULLSCREEN_DESKTOP); 264 | SDL_GetWindowSize(clock->window, &clock->w, &clock->h); 265 | LOG_DEBUG("Set clock `%d` to fullscreen with size `%dx%d`.\n", 266 | clock->i, clock->w, clock->h); 267 | } else { 268 | SDL_SetWindowFullscreen(clock->window, 0); 269 | /** 270 | * We need to restore window first, because if started in 271 | * fullscreen mode, it will be maximized when turning off 272 | * fullscreen mode and we cannot set window size. Looks like 273 | * a strange bug. 274 | */ 275 | SDL_RestoreWindow(clock->window); 276 | SDL_SetWindowSize(clock->window, WINDOW_WIDTH, WINDOW_HEIGHT); 277 | SDL_SetWindowPosition( 278 | clock->window, 279 | display_bounds.x + 280 | (display_bounds.w - WINDOW_WIDTH) / 2, 281 | display_bounds.y + 282 | (display_bounds.h - WINDOW_HEIGHT) / 2); 283 | SDL_GetWindowSize(clock->window, &clock->w, &clock->h); 284 | LOG_DEBUG("Set clock `%d` to windowed.\n", clock->i); 285 | } 286 | // Toggling fullscreen always changes size. 287 | _flipclock_clock_update_layout(clock); 288 | } 289 | 290 | void flipclock_clock_set_hour(struct flipclock_clock *clock, const char hour[], 291 | bool flip) 292 | { 293 | // Text can be NULL to clear card. 294 | RETURN_IF_FAIL(clock != NULL); 295 | 296 | flipclock_card_set_text(clock->hour, hour); 297 | if (flip) 298 | flipclock_card_flip(clock->hour); 299 | } 300 | 301 | void flipclock_clock_set_minute(struct flipclock_clock *clock, 302 | const char minute[], bool flip) 303 | { 304 | // Text can be NULL to clear card. 305 | RETURN_IF_FAIL(clock != NULL); 306 | 307 | flipclock_card_set_text(clock->minute, minute); 308 | if (flip) 309 | flipclock_card_flip(clock->minute); 310 | } 311 | 312 | void flipclock_clock_set_second(struct flipclock_clock *clock, 313 | const char second[], bool flip) 314 | { 315 | // Text can be NULL to clear card. 316 | RETURN_IF_FAIL(clock != NULL); 317 | 318 | if (!clock->app->show_second) 319 | return; 320 | 321 | flipclock_card_set_text(clock->second, second); 322 | if (flip) 323 | flipclock_card_flip(clock->second); 324 | } 325 | 326 | void flipclock_clock_set_ampm(struct flipclock_clock *clock, const char ampm[]) 327 | { 328 | // Text can be NULL to clear card. 329 | RETURN_IF_FAIL(clock != NULL); 330 | 331 | flipclock_card_set_sub_text(clock->hour, ampm); 332 | // Set ampm should never flip a card. 333 | } 334 | 335 | void flipclock_clock_handle_window_event(struct flipclock_clock *clock, 336 | SDL_Event event) 337 | { 338 | RETURN_IF_FAIL(clock != NULL); 339 | 340 | const struct flipclock *app = clock->app; 341 | int clock_i = clock->i; 342 | switch (event.window.event) { 343 | case SDL_WINDOWEVENT_SIZE_CHANGED: 344 | /** 345 | * Only re-render when size changed. 346 | * Windows may send event when size 347 | * not changed, and cause strange bugs. 348 | */ 349 | if (event.window.data1 != clock->w || 350 | event.window.data2 != clock->h) { 351 | clock->w = event.window.data1; 352 | clock->h = event.window.data2; 353 | LOG_DEBUG("New window size for " 354 | "clock `%d` is `%dx%d`.\n", 355 | clock->i, clock->w, clock->h); 356 | _flipclock_clock_update_layout(clock); 357 | } 358 | break; 359 | case SDL_WINDOWEVENT_MINIMIZED: 360 | clock->waiting = true; 361 | break; 362 | // `RESTORED` is emitted after `MINIMIZED`. 363 | case SDL_WINDOWEVENT_RESTORED: 364 | clock->waiting = false; 365 | /** 366 | * Sometimes when a window is restored, its texture get lost. 367 | * Typically happens when we have two fullscreen clocks in 368 | * one display, and the lower one is switched to top, and we 369 | * have to redraw its texture. 370 | */ 371 | // XXX: It seems OK without redrawing. 372 | // flipclock_clock_draw(clock); 373 | break; 374 | case SDL_WINDOWEVENT_CLOSE: 375 | flipclock_clock_destroy(clock); 376 | app->clocks[clock_i] = NULL; 377 | /** 378 | * See . 379 | * It seems that SDL will send SDL_QUIT automatically 380 | * when all windows are closed, so we don't need to exit 381 | * manually here. 382 | */ 383 | LOG_DEBUG("Clock `%d` closed!\n", clock_i); 384 | break; 385 | default: 386 | break; 387 | } 388 | } 389 | 390 | void flipclock_clock_animate(struct flipclock_clock *clock) 391 | { 392 | RETURN_IF_FAIL(clock != NULL); 393 | 394 | const struct flipclock *app = clock->app; 395 | SDL_SetRenderDrawColor(clock->renderer, app->background_color.r, 396 | app->background_color.g, app->background_color.b, 397 | app->background_color.a); 398 | SDL_RenderClear(clock->renderer); 399 | 400 | flipclock_card_animate(clock->hour); 401 | flipclock_card_animate(clock->minute); 402 | if (app->show_second) 403 | flipclock_card_animate(clock->second); 404 | 405 | SDL_RenderPresent(clock->renderer); 406 | } 407 | 408 | void flipclock_clock_destroy(struct flipclock_clock *clock) 409 | { 410 | RETURN_IF_FAIL(clock != NULL); 411 | 412 | flipclock_card_destory(clock->hour); 413 | flipclock_card_destory(clock->minute); 414 | if (clock->second != NULL) 415 | flipclock_card_destory(clock->second); 416 | SDL_DestroyRenderer(clock->renderer); 417 | SDL_DestroyWindow(clock->window); 418 | free(clock); 419 | } 420 | -------------------------------------------------------------------------------- /srcs/card.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include "flipclock.h" 8 | #include "card.h" 9 | 10 | #define PI 3.1415927 11 | #define MAX_PROGRESS 300 12 | #define HALF_PROGRESS (MAX_PROGRESS / 2) 13 | 14 | struct flipclock_card *flipclock_card_create(struct flipclock *app, 15 | SDL_Renderer *renderer) 16 | { 17 | RETURN_VAL_IF_FAIL(app != NULL, NULL); 18 | RETURN_VAL_IF_FAIL(renderer != NULL, NULL); 19 | 20 | struct flipclock_card *card = malloc(sizeof(*card)); 21 | if (card == NULL) { 22 | LOG_ERROR("Failed to create card!"); 23 | exit(EXIT_FAILURE); 24 | } 25 | card->app = app; 26 | card->renderer = renderer; 27 | card->current = NULL; 28 | card->previous = NULL; 29 | card->should_redraw = false; 30 | card->start_tick = 0; 31 | card->text[0] = '\0'; 32 | card->font = NULL; 33 | card->has_sub_text = false; 34 | card->sub_text[0] = '\0'; 35 | card->sub_font = NULL; 36 | card->divider_height = 0; 37 | card->rect.w = 0; 38 | card->rect.h = 0; 39 | return card; 40 | } 41 | 42 | static void _flipclock_card_create_textures(struct flipclock_card *card) 43 | { 44 | RETURN_IF_FAIL(card != NULL); 45 | 46 | LOG_DEBUG("Creating new textures with size `%dx%d`.\n", card->rect.w, 47 | card->rect.h); 48 | card->current = SDL_CreateTexture(card->renderer, 0, 49 | SDL_TEXTUREACCESS_TARGET, 50 | card->rect.w, card->rect.h); 51 | if (card->current == NULL) { 52 | LOG_ERROR("%s\n", SDL_GetError()); 53 | exit(EXIT_FAILURE); 54 | } 55 | SDL_SetTextureBlendMode(card->current, SDL_BLENDMODE_BLEND); 56 | card->previous = SDL_CreateTexture(card->renderer, 0, 57 | SDL_TEXTUREACCESS_TARGET, 58 | card->rect.w, card->rect.h); 59 | if (card->previous == NULL) { 60 | LOG_ERROR("%s\n", SDL_GetError()); 61 | exit(EXIT_FAILURE); 62 | } 63 | SDL_SetTextureBlendMode(card->previous, SDL_BLENDMODE_BLEND); 64 | } 65 | 66 | static void _flipclock_card_destroy_textures(struct flipclock_card *card) 67 | { 68 | RETURN_IF_FAIL(card != NULL); 69 | 70 | LOG_DEBUG("Destroying old textures.\n"); 71 | if (card->current != NULL) { 72 | SDL_DestroyTexture(card->current); 73 | card->current = NULL; 74 | } 75 | if (card->previous != NULL) { 76 | SDL_DestroyTexture(card->previous); 77 | card->previous = NULL; 78 | } 79 | } 80 | 81 | // TODO: Only open sub font if sub text used. 82 | static void _flipclock_card_open_fonts(struct flipclock_card *card) 83 | { 84 | RETURN_IF_FAIL(card != NULL); 85 | 86 | const struct flipclock *app = card->app; 87 | LOG_DEBUG("Opening font from `%s`.\n", app->font_path); 88 | card->font = 89 | TTF_OpenFont(app->font_path, card->rect.h * app->text_scale); 90 | card->sub_font = TTF_OpenFont(app->font_path, 91 | card->sub_rect.h * app->text_scale); 92 | if (card->font == NULL || card->sub_font == NULL) { 93 | LOG_ERROR("%s\n", TTF_GetError()); 94 | exit(EXIT_FAILURE); 95 | } 96 | } 97 | 98 | static void _flipclock_card_close_fonts(struct flipclock_card *card) 99 | { 100 | RETURN_IF_FAIL(card != NULL); 101 | 102 | LOG_DEBUG("Closing old font.\n"); 103 | if (card->font != NULL) { 104 | TTF_CloseFont(card->font); 105 | card->font = NULL; 106 | } 107 | if (card->sub_font != NULL) { 108 | TTF_CloseFont(card->sub_font); 109 | card->sub_font = NULL; 110 | } 111 | } 112 | 113 | static void _flipclock_card_clear_current_texture(struct flipclock_card *card) 114 | { 115 | RETURN_IF_FAIL(card != NULL); 116 | 117 | SDL_SetRenderTarget(card->renderer, card->current); 118 | // Always clear texture with transparent so rounded corner will be fine. 119 | SDL_SetRenderDrawColor(card->renderer, 0x00, 0x00, 0x00, 0x00); 120 | SDL_RenderClear(card->renderer); 121 | SDL_SetRenderTarget(card->renderer, NULL); 122 | } 123 | 124 | static void _flipclock_card_draw_rounded_box(struct flipclock_card *card) 125 | { 126 | RETURN_IF_FAIL(card != NULL); 127 | 128 | const struct flipclock *app = card->app; 129 | // Card-local position. 130 | const SDL_Rect box_rect = { 0, 0, card->rect.w, card->rect.h }; 131 | if (2 * card->radius > box_rect.w) 132 | card->radius = box_rect.w / 2; 133 | if (2 * card->radius > box_rect.h) 134 | card->radius = box_rect.h / 2; 135 | // Worst case: a normal rect. 136 | if (card->radius <= 1) { 137 | SDL_SetRenderTarget(card->renderer, card->current); 138 | SDL_SetRenderDrawColor(card->renderer, app->box_color.r, 139 | app->box_color.g, app->box_color.b, 140 | app->box_color.a); 141 | SDL_RenderFillRect(card->renderer, &box_rect); 142 | SDL_SetRenderTarget(card->renderer, NULL); 143 | return; 144 | } 145 | 146 | SDL_SetRenderTarget(card->renderer, card->current); 147 | SDL_SetRenderDrawColor(card->renderer, app->box_color.r, 148 | app->box_color.g, app->box_color.b, 149 | app->box_color.a); 150 | int x = 0; 151 | int y = card->radius; 152 | int d = 3 - 2 * card->radius; 153 | while (x <= y) { 154 | SDL_RenderDrawLine( 155 | card->renderer, box_rect.x + card->radius - x, 156 | box_rect.y + card->radius - y, 157 | box_rect.x + box_rect.w - card->radius + x - 1, 158 | box_rect.y + card->radius - y); 159 | SDL_RenderDrawLine( 160 | card->renderer, box_rect.x + card->radius - x, 161 | box_rect.y + box_rect.h - card->radius + y, 162 | box_rect.x + box_rect.w - card->radius + x - 1, 163 | box_rect.y + box_rect.h - card->radius + y); 164 | SDL_RenderDrawLine( 165 | card->renderer, box_rect.x + card->radius - y, 166 | box_rect.y + card->radius - x, 167 | box_rect.x + box_rect.w - card->radius + y - 1, 168 | box_rect.y + card->radius - x); 169 | SDL_RenderDrawLine( 170 | card->renderer, box_rect.x + card->radius - y, 171 | box_rect.y + box_rect.h - card->radius + x, 172 | box_rect.x + box_rect.w - card->radius + y - 1, 173 | box_rect.y + box_rect.h - card->radius + x); 174 | if (d < 0) { 175 | d = d + 4 * x + 6; 176 | } else { 177 | d = d + 4 * (x - y) + 10; 178 | --y; 179 | } 180 | ++x; 181 | } 182 | SDL_Rect temp_rect; 183 | temp_rect.x = box_rect.x; 184 | temp_rect.y = box_rect.y + card->radius; 185 | temp_rect.w = box_rect.w; 186 | temp_rect.h = box_rect.h - 2 * card->radius; 187 | SDL_RenderFillRect(card->renderer, &temp_rect); 188 | SDL_SetRenderTarget(card->renderer, NULL); 189 | } 190 | 191 | // A special text drawing function, will draw all chars as mono. 192 | static void _draw_text(SDL_Renderer *renderer, SDL_Texture *target_texture, 193 | SDL_Rect target_rect, TTF_Font *font, SDL_Color color, 194 | const char text[]) 195 | { 196 | RETURN_IF_FAIL(renderer != NULL); 197 | RETURN_IF_FAIL(target_texture != NULL); 198 | RETURN_IF_FAIL(font != NULL); 199 | RETURN_IF_FAIL(text != NULL); 200 | 201 | int len = strlen(text); 202 | LOG_DEBUG("Drawing text `%s`.\n", text); 203 | SDL_SetRenderTarget(renderer, target_texture); 204 | for (int i = 0; i < len; ++i) { 205 | /** 206 | * See . 207 | * Normally shaded is enough, however we have a rounded box, 208 | * and many fonts' boxes are too big compared with their 209 | * characters, they just cover the rounded corner. 210 | * So I have to use blended mode, because solid mode does not 211 | * have anti-alias. 212 | */ 213 | SDL_Surface *text_surface = 214 | TTF_RenderGlyph_Blended(font, text[i], color); 215 | if (text_surface == NULL) { 216 | LOG_ERROR("%s\n", TTF_GetError()); 217 | exit(EXIT_FAILURE); 218 | } 219 | SDL_Texture *text_texture = 220 | SDL_CreateTextureFromSurface(renderer, text_surface); 221 | if (text_texture == NULL) { 222 | LOG_ERROR("%s\n", TTF_GetError()); 223 | exit(EXIT_FAILURE); 224 | } 225 | SDL_Rect text_rect; 226 | text_rect.x = target_rect.x + target_rect.w / len * i + 227 | (target_rect.w / len - text_surface->w) / 2; 228 | text_rect.y = 229 | target_rect.y + (target_rect.h - text_surface->h) / 2; 230 | text_rect.w = text_surface->w; 231 | text_rect.h = text_surface->h; 232 | SDL_FreeSurface(text_surface); 233 | SDL_RenderCopy(renderer, text_texture, NULL, &text_rect); 234 | SDL_DestroyTexture(text_texture); 235 | } 236 | SDL_SetRenderTarget(renderer, NULL); 237 | } 238 | 239 | static void _flipclock_card_draw_text(struct flipclock_card *card) 240 | { 241 | RETURN_IF_FAIL(card != NULL); 242 | 243 | const struct flipclock *app = card->app; 244 | // Card-local position. 245 | const SDL_Rect box_rect = { 0, 0, card->rect.w, card->rect.h }; 246 | _draw_text(card->renderer, card->current, box_rect, card->font, 247 | app->text_color, card->text); 248 | if (card->has_sub_text) { 249 | _draw_text(card->renderer, card->current, card->sub_rect, 250 | card->sub_font, app->text_color, card->sub_text); 251 | } 252 | } 253 | 254 | static void _flipclock_card_draw_divider(struct flipclock_card *card) 255 | { 256 | RETURN_IF_FAIL(card != NULL); 257 | 258 | const struct flipclock *app = card->app; 259 | SDL_Rect divider_rect = { 0, (card->rect.h - card->divider_height) / 2, 260 | card->rect.w, card->divider_height }; 261 | SDL_SetRenderTarget(card->renderer, card->current); 262 | // Don't be transparent, or you will not see divider, it's over card. 263 | SDL_SetRenderDrawColor(card->renderer, app->background_color.r, 264 | app->background_color.g, app->background_color.b, 265 | app->background_color.a); 266 | SDL_RenderFillRect(card->renderer, ÷r_rect); 267 | SDL_SetRenderTarget(card->renderer, NULL); 268 | } 269 | 270 | static void _flipclock_card_draw(struct flipclock_card *card) 271 | { 272 | RETURN_IF_FAIL(card != NULL); 273 | 274 | // Always do texture swap before drawing. 275 | SDL_Texture *swap = card->current; 276 | card->current = card->previous; 277 | card->previous = swap; 278 | 279 | _flipclock_card_clear_current_texture(card); 280 | 281 | LOG_DEBUG("Drawing card.\n"); 282 | _flipclock_card_draw_rounded_box(card); 283 | _flipclock_card_draw_text(card); 284 | _flipclock_card_draw_divider(card); 285 | } 286 | 287 | // Those setter functions will request redraw. 288 | void flipclock_card_set_rect(struct flipclock_card *card, const SDL_Rect rect) 289 | { 290 | RETURN_IF_FAIL(card != NULL); 291 | 292 | card->divider_height = rect.h / 100; 293 | card->radius = rect.h / 10; 294 | card->sub_rect.h = rect.h / 10; 295 | // Sub text's width is decide by the height. 296 | card->sub_rect.w = card->sub_rect.h * strlen(card->sub_text); 297 | // This should be a card-local position, so don't add rect's x and y. 298 | card->sub_rect.x = rect.h / 50; 299 | card->sub_rect.y = rect.h - rect.h / 35 - card->sub_rect.h; 300 | // Reload textures and fonts if size changed. 301 | SDL_Rect old_rect = card->rect; 302 | card->rect = rect; 303 | if (card->rect.w != old_rect.w || card->rect.h != old_rect.h) { 304 | _flipclock_card_close_fonts(card); 305 | _flipclock_card_open_fonts(card); 306 | _flipclock_card_destroy_textures(card); 307 | _flipclock_card_create_textures(card); 308 | // A redraw is requested because size changed. 309 | card->should_redraw = true; 310 | } 311 | } 312 | 313 | void flipclock_card_set_text(struct flipclock_card *card, const char text[]) 314 | { 315 | // Text can be NULL to clear card. 316 | RETURN_IF_FAIL(card != NULL); 317 | 318 | // card->text_changed = true; 319 | if (text == NULL) { 320 | card->text[0] = '\0'; 321 | } else { 322 | strncpy(card->text, text, MAX_TEXT_LENGTH); 323 | card->text[MAX_TEXT_LENGTH - 1] = '\0'; 324 | } 325 | 326 | // TODO: Copy and compare text internally. 327 | // So we won't update card when we have many cards displaying a word 328 | // and change word. 329 | 330 | /** 331 | * Setting text always requests a redraw, but not always requests a 332 | * flipping. You don't want to flip when you change ampm. 333 | */ 334 | card->should_redraw = true; 335 | } 336 | 337 | void flipclock_card_set_sub_text(struct flipclock_card *card, 338 | const char sub_text[]) 339 | { 340 | // Text can be NULL to clear card. 341 | RETURN_IF_FAIL(card != NULL); 342 | 343 | if (sub_text == NULL) { 344 | card->has_sub_text = false; 345 | card->sub_text[0] = '\0'; 346 | } else { 347 | card->has_sub_text = true; 348 | strncpy(card->sub_text, sub_text, MAX_TEXT_LENGTH); 349 | card->sub_text[MAX_TEXT_LENGTH - 1] = '\0'; 350 | } 351 | // Sub text length might be changed so re-calculate it. 352 | card->sub_rect.w = card->sub_rect.h * strlen(card->sub_text); 353 | 354 | /** 355 | * Setting text always requests a redraw, but not always requests a 356 | * flipping. You don't want to flip when you change ampm. 357 | */ 358 | card->should_redraw = true; 359 | } 360 | 361 | void flipclock_card_flip(struct flipclock_card *card) 362 | { 363 | RETURN_IF_FAIL(card != NULL); 364 | 365 | // Flipping animation start. 366 | card->start_tick = SDL_GetTicks(); 367 | } 368 | 369 | void flipclock_card_animate(struct flipclock_card *card) 370 | { 371 | RETURN_IF_FAIL(card != NULL); 372 | 373 | /** 374 | * We defer redraw requests to actually copy, so we only redraw card 375 | * once for different text changes. 376 | */ 377 | if (card->should_redraw) { 378 | _flipclock_card_draw(card); 379 | card->should_redraw = false; 380 | } 381 | 382 | // Do the flipping animation by copy card to window's given position. 383 | 384 | long long progress = SDL_GetTicks() - card->start_tick; 385 | // Don't animate when program just started. 386 | if (progress >= MAX_PROGRESS || card->start_tick == 0) { 387 | // It finished flipping, so we don't draw flipping animation. 388 | // Card-local position. 389 | SDL_Rect card_local_rect = { 0, 0, card->rect.w, card->rect.h }; 390 | SDL_RenderCopy(card->renderer, card->current, &card_local_rect, 391 | &card->rect); 392 | return; 393 | } 394 | 395 | // Copy the upper current digit. 396 | // Card-local position for source. 397 | SDL_Rect half_source_rect = { 0, 0, card->rect.w, card->rect.h / 2 }; 398 | SDL_Rect half_target_rect = { card->rect.x, card->rect.y, card->rect.w, 399 | card->rect.h / 2 }; 400 | SDL_RenderCopy(card->renderer, card->current, &half_source_rect, 401 | &half_target_rect); 402 | 403 | // Copy the lower previous digit. 404 | half_source_rect.y = card->rect.h / 2; 405 | half_target_rect.y = card->rect.y + card->rect.h / 2; 406 | SDL_RenderCopy(card->renderer, card->previous, &half_source_rect, 407 | &half_target_rect); 408 | 409 | /** 410 | * Copy the flipping part. 411 | * Upper half is previous and lower half is current. 412 | * Just custom the destination Rect, zoom will be done automatically. 413 | */ 414 | bool upper_half = progress <= HALF_PROGRESS; 415 | double angle = upper_half ? 416 | PI * progress / MAX_PROGRESS : 417 | PI * (1.0 - (double)progress / MAX_PROGRESS); 418 | double scale = cos(angle); 419 | half_source_rect.y = upper_half ? 0 : card->rect.h / 2; 420 | half_target_rect.y = 421 | card->rect.y + (upper_half ? 422 | (double)card->rect.h / 2 * (1 - scale) : 423 | (double)card->rect.h / 2); 424 | half_target_rect.h = (double)card->rect.h / 2 * scale; 425 | SDL_RenderCopy(card->renderer, 426 | upper_half ? card->previous : card->current, 427 | &half_source_rect, &half_target_rect); 428 | } 429 | 430 | void flipclock_card_destory(struct flipclock_card *card) 431 | { 432 | RETURN_IF_FAIL(card != NULL); 433 | 434 | _flipclock_card_close_fonts(card); 435 | _flipclock_card_destroy_textures(card); 436 | free(card); 437 | } 438 | -------------------------------------------------------------------------------- /srcs/flipclock.c: -------------------------------------------------------------------------------- 1 | /** 2 | * Alynx Zhou (https://alynx.one/) 3 | */ 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "getarg.h" 10 | #include "flipclock.h" 11 | #include "clock.h" 12 | #include "card.h" 13 | 14 | #define WINDOW_WIDTH 800 15 | #define WINDOW_HEIGHT 600 16 | #define FPS 60 17 | #define MAX_PROGRESS 300 18 | #define HALF_PROGRESS (MAX_PROGRESS / 2) 19 | #define DOUBLE_TAP_INTERVAL_MS 300 20 | 21 | #if defined(_WIN32) 22 | static void _flipclock_get_program_dir_win32(char program_dir[]) 23 | { 24 | RETURN_IF_FAIL(program_dir != NULL); 25 | 26 | /** 27 | * See . 28 | * GetModuleFileName is a macro to 29 | * GetModuleFileNameW and GetModuleFileNameA. 30 | */ 31 | GetModuleFileName(NULL, program_dir, MAX_BUFFER_LENGTH); 32 | program_dir[MAX_BUFFER_LENGTH - 1] = '\0'; 33 | if (strlen(program_dir) == MAX_BUFFER_LENGTH - 1) 34 | LOG_ERROR("`program_dir` too long, may fail to load.\n"); 35 | /** 36 | * Remove the program name and get the dir. 37 | * This should work because Windows path should have `\\`. 38 | */ 39 | for (int i = strlen(program_dir) - 1; i >= 0; --i) { 40 | if (program_dir[i] == '\\') { 41 | program_dir[i] = '\0'; 42 | break; 43 | } 44 | } 45 | } 46 | #endif 47 | 48 | struct flipclock *flipclock_create(void) 49 | { 50 | struct flipclock *app = malloc(sizeof(*app)); 51 | if (app == NULL) { 52 | LOG_ERROR("Failed to create app!\n"); 53 | exit(EXIT_FAILURE); 54 | } 55 | app->clocks = NULL; 56 | // Should create 1 clock in windowed mode. 57 | app->clocks_length = 1; 58 | app->last_touch_time = 0; 59 | app->last_touch_finger = 0; 60 | app->running = true; 61 | app->text_color.r = 0xd0; 62 | app->text_color.g = 0xd0; 63 | app->text_color.b = 0xd0; 64 | app->text_color.a = 0xff; 65 | app->box_color.r = 0x20; 66 | app->box_color.g = 0x20; 67 | app->box_color.b = 0x20; 68 | app->box_color.a = 0xff; 69 | app->background_color.r = 0x00; 70 | app->background_color.g = 0x00; 71 | app->background_color.b = 0x00; 72 | app->background_color.a = 0xff; 73 | app->ampm = false; 74 | app->full = true; 75 | app->show_second = false; 76 | app->font_path[0] = '\0'; 77 | app->conf_path[0] = '\0'; 78 | app->text_scale = 1.0; 79 | app->card_scale = 1.0; 80 | #if defined(_WIN32) 81 | app->preview = false; 82 | app->screensaver = false; 83 | app->program_dir[0] = '\0'; 84 | _flipclock_get_program_dir_win32(app->program_dir); 85 | LOG_DEBUG("Using `program_dir` `%s`.\n", app->program_dir); 86 | #endif 87 | #if defined(_WIN32) 88 | snprintf(app->font_path, MAX_BUFFER_LENGTH, "%s\\flipclock.ttf", 89 | app->program_dir); 90 | #elif defined(__ANDROID__) 91 | // Directly under `app/src/main/assets` for Android APP. 92 | strncpy(app->font_path, "flipclock.ttf", MAX_BUFFER_LENGTH); 93 | #elif defined(__linux__) && !defined(__ANDROID__) 94 | strncpy(app->font_path, PACKAGE_DATADIR "/fonts/flipclock.ttf", 95 | MAX_BUFFER_LENGTH); 96 | #endif 97 | app->font_path[MAX_BUFFER_LENGTH - 1] = '\0'; 98 | if (strlen(app->font_path) == MAX_BUFFER_LENGTH - 1) 99 | LOG_ERROR("`font_path` too long, may fail to load.\n"); 100 | time_t raw_time = time(NULL); 101 | app->now = *localtime(&raw_time); 102 | return app; 103 | } 104 | 105 | static int _flipclock_parse_key_value(char line[], char **key, char **value) 106 | { 107 | RETURN_VAL_IF_FAIL(line != NULL, -6); 108 | RETURN_VAL_IF_FAIL(key != NULL, -7); 109 | RETURN_VAL_IF_FAIL(value != NULL, -8); 110 | 111 | const int line_length = strlen(line); 112 | int key_start = -1; 113 | for (int i = 0; i < line_length; ++i) { 114 | if (!isspace(line[i])) { 115 | key_start = i; 116 | break; 117 | } 118 | } 119 | if (key_start == -1) { 120 | LOG_DEBUG("No `key_start` found! Skip empty line.\n"); 121 | return -1; 122 | } 123 | // Only handle line start, this makes it easier for colors. 124 | if (line[key_start] == '#' || line[key_start] == ';') { 125 | LOG_DEBUG("Skip comment line.\n"); 126 | return 1; 127 | } 128 | int value_end = -1; 129 | for (int i = line_length - 1; i >= 0; --i) { 130 | if (!isspace(line[i])) { 131 | value_end = i + 1; 132 | break; 133 | } 134 | } 135 | if (value_end == -1) { 136 | LOG_DEBUG("No `value_end` found! Skip empty line.\n"); 137 | return -2; 138 | } 139 | line[value_end] = '\0'; 140 | int equal = -1; 141 | for (int i = 0; i < line_length; ++i) { 142 | if (line[i] == '=') { 143 | equal = i; 144 | break; 145 | } 146 | } 147 | if (equal == -1) { 148 | LOG_ERROR("No `=` found! Invalid line!\n"); 149 | return -3; 150 | } 151 | int key_end = -1; 152 | for (int i = equal - 1; i >= key_start; --i) { 153 | if (!isspace(line[i])) { 154 | key_end = i + 1; 155 | break; 156 | } 157 | } 158 | if (key_end == -1) { 159 | LOG_ERROR("No `key_end` found! Invalid line!\n"); 160 | return -4; 161 | } 162 | line[key_end] = '\0'; 163 | int value_start = -1; 164 | for (int i = equal + 1; i < value_end; ++i) { 165 | if (!isspace(line[i])) { 166 | value_start = i; 167 | break; 168 | } 169 | } 170 | if (value_start == -1) { 171 | LOG_ERROR("No `value_start` found! Invalid line!\n"); 172 | return -5; 173 | } 174 | *key = line + key_start; 175 | *value = line + value_start; 176 | return 0; 177 | } 178 | 179 | static int _flipclock_parse_color(const char rgba[], SDL_Color *color) 180 | { 181 | RETURN_VAL_IF_FAIL(rgba != NULL, -5); 182 | RETURN_VAL_IF_FAIL(color != NULL, -6); 183 | 184 | const int rgba_length = strlen(rgba); 185 | if (rgba_length == 0) { 186 | LOG_ERROR("Empty color string!\n"); 187 | return -1; 188 | } else if (rgba[0] != '#') { 189 | LOG_ERROR("Color string must start with `#`!\n"); 190 | return -2; 191 | } else if (rgba_length != 7 && rgba_length != 9) { 192 | LOG_ERROR("Color string must be in format `#rrggbb[aa]`!\n"); 193 | return -3; 194 | } else { 195 | for (int i = 1; i < rgba_length; ++i) { 196 | // Cool, ctype.h always gives me surprise. 197 | if (!isxdigit(rgba[i])) { 198 | LOG_ERROR("Color string numbers " 199 | "should be hexcode!\n"); 200 | return -4; 201 | } 202 | } 203 | /** 204 | * Even if user input an invalid hexcode, 205 | * we also let strtoll try to parse it. 206 | * It's user's problem when displayed color 207 | * is not what he/she wants. 208 | */ 209 | long long hex_number = strtoll(rgba + 1, NULL, 16); 210 | // Add 0xff as alpha if no alpha provided. 211 | if (rgba_length == 7) 212 | hex_number = (hex_number << 8) | 0xff; 213 | color->r = (hex_number >> 24) & 0xff; 214 | color->g = (hex_number >> 16) & 0xff; 215 | color->b = (hex_number >> 8) & 0xff; 216 | color->a = (hex_number >> 0) & 0xff; 217 | LOG_DEBUG("Parsed color `rgba(%d, %d, %d, %d)`.\n", color->r, 218 | color->g, color->b, color->a); 219 | } 220 | return 0; 221 | } 222 | 223 | #if defined(_WIN32) 224 | static FILE *_flipclock_open_conf_win32(char conf_path[], 225 | const char program_dir[]) 226 | { 227 | RETURN_VAL_IF_FAIL(conf_path != NULL, NULL); 228 | RETURN_VAL_IF_FAIL(program_dir != NULL, NULL); 229 | 230 | snprintf(conf_path, MAX_BUFFER_LENGTH, "%s\\flipclock.conf", 231 | program_dir); 232 | conf_path[MAX_BUFFER_LENGTH - 1] = '\0'; 233 | if (strlen(conf_path) == MAX_BUFFER_LENGTH - 1) 234 | LOG_ERROR("`conf_path` too long, may fail to load.\n"); 235 | return fopen(conf_path, "r"); 236 | } 237 | #elif defined(__linux__) && !defined(__ANDROID__) 238 | static FILE *_flipclock_open_conf_linux(char *conf_path) 239 | { 240 | RETURN_VAL_IF_FAIL(conf_path != NULL, NULL); 241 | 242 | FILE *conf = NULL; 243 | 244 | // Be a good program. 245 | const char *conf_dir = getenv("XDG_CONFIG_HOME"); 246 | if (conf_dir != NULL && strlen(conf_dir) != 0) { 247 | snprintf(conf_path, MAX_BUFFER_LENGTH, "%s/flipclock.conf", 248 | conf_dir); 249 | conf_path[MAX_BUFFER_LENGTH - 1] = '\0'; 250 | if (strlen(conf_path) == MAX_BUFFER_LENGTH - 1) 251 | LOG_ERROR("`conf_path` too long, may fail to load.\n"); 252 | conf = fopen(conf_path, "r"); 253 | if (conf != NULL) 254 | return conf; 255 | } 256 | 257 | // Linux users should not be homeless. But we may in sandbox. 258 | const char *home = getenv("HOME"); 259 | if (home != NULL && strlen(home) != 0) { 260 | snprintf(conf_path, MAX_BUFFER_LENGTH, 261 | "%s/.config/flipclock.conf", home); 262 | conf_path[MAX_BUFFER_LENGTH - 1] = '\0'; 263 | if (strlen(conf_path) == MAX_BUFFER_LENGTH - 1) 264 | LOG_ERROR("`conf_path` too long, may fail to load.\n"); 265 | conf = fopen(conf_path, "r"); 266 | if (conf != NULL) 267 | return conf; 268 | } 269 | 270 | strncpy(conf_path, PACKAGE_SYSCONFDIR "/flipclock.conf", 271 | MAX_BUFFER_LENGTH); 272 | conf_path[MAX_BUFFER_LENGTH - 1] = '\0'; 273 | if (strlen(conf_path) == MAX_BUFFER_LENGTH - 1) 274 | LOG_ERROR("`conf_path` too long, may fail to load.\n"); 275 | return fopen(conf_path, "r"); 276 | } 277 | #endif 278 | 279 | static void _flipclock_apply_key_value(struct flipclock *app, const char key[], 280 | const char value[]) 281 | { 282 | RETURN_IF_FAIL(app != NULL); 283 | RETURN_IF_FAIL(key != NULL); 284 | RETURN_IF_FAIL(value != NULL); 285 | 286 | /** 287 | * It's better to use a temp variable here, 288 | * so when parsing failed we still have the default color. 289 | */ 290 | SDL_Color parsed_color; 291 | if (!strcmp(key, "ampm")) { 292 | if (!strcmp(value, "true")) 293 | app->ampm = true; 294 | } else if (!strcmp(key, "full")) { 295 | if (!strcmp(value, "false")) 296 | app->full = false; 297 | } else if (!strcmp(key, "show_second")) { 298 | if (!strcmp(value, "true")) 299 | app->show_second = true; 300 | } else if (!strcmp(key, "font")) { 301 | strncpy(app->font_path, value, MAX_BUFFER_LENGTH); 302 | app->font_path[MAX_BUFFER_LENGTH - 1] = '\0'; 303 | if (strlen(app->font_path) == MAX_BUFFER_LENGTH - 1) 304 | LOG_ERROR("`font_path` too long, may fail to load.\n"); 305 | } else if (!strcmp(key, "text_scale")) { 306 | app->text_scale = strtod(value, NULL); 307 | } else if (!strcmp(key, "font_scale")) { 308 | // Backward compatibility for deprecated keys. 309 | LOG_ERROR("`font_scale` is deprecated, " 310 | "use `text_scale` instead.\n"); 311 | app->text_scale = strtod(value, NULL); 312 | } else if (!strcmp(key, "card_scale")) { 313 | app->card_scale = strtod(value, NULL); 314 | } else if (!strcmp(key, "rect_scale")) { 315 | // Backward compatibility for deprecated keys. 316 | LOG_ERROR("`rect_scale` is deprecated, " 317 | "use `card_scale` instead.\n"); 318 | app->card_scale = strtod(value, NULL); 319 | } else if (!strcmp(key, "text_color")) { 320 | if (!_flipclock_parse_color(value, &parsed_color)) 321 | app->text_color = parsed_color; 322 | else 323 | LOG_ERROR("Failed to parse `text_color`!\n"); 324 | } else if (!strcmp(key, "font_color")) { 325 | // Backward compatibility for deprecated keys. 326 | LOG_ERROR("`font_color` is deprecated, " 327 | "use `text_color` instead.\n"); 328 | if (!_flipclock_parse_color(value, &parsed_color)) 329 | app->text_color = parsed_color; 330 | else 331 | LOG_ERROR("Failed to parse `text_color`!\n"); 332 | } else if (!strcmp(key, "box_color")) { 333 | if (!_flipclock_parse_color(value, &parsed_color)) 334 | app->box_color = parsed_color; 335 | else 336 | LOG_ERROR("Failed to parse `box_color`!\n"); 337 | } else if (!strcmp(key, "rect_color")) { 338 | // Backward compatibility for deprecated keys. 339 | LOG_ERROR("`rect_color` is deprecated, " 340 | "use `box_color` instead.\n"); 341 | if (!_flipclock_parse_color(value, &parsed_color)) 342 | app->box_color = parsed_color; 343 | else 344 | LOG_ERROR("Failed to parse `box_color`!\n"); 345 | } else if (!strcmp(key, "background_color")) { 346 | if (!_flipclock_parse_color(value, &parsed_color)) 347 | app->background_color = parsed_color; 348 | else 349 | LOG_ERROR("Failed to parse `background_color`!\n"); 350 | } else if (!strcmp(key, "back_color")) { 351 | // Backward compatibility for deprecated keys. 352 | LOG_ERROR("`back_color` is deprecated, " 353 | "use `background_color` instead.\n"); 354 | if (!_flipclock_parse_color(value, &parsed_color)) 355 | app->background_color = parsed_color; 356 | else 357 | LOG_ERROR("Failed to parse `background_color`!\n"); 358 | } else { 359 | LOG_ERROR("Unknown key `%s`.\n", key); 360 | } 361 | } 362 | 363 | void flipclock_load_conf(struct flipclock *app) 364 | { 365 | RETURN_IF_FAIL(app != NULL); 366 | 367 | FILE *conf = NULL; 368 | #if defined(_WIN32) 369 | conf = _flipclock_open_conf_win32(app->conf_path, app->program_dir); 370 | #elif defined(__linux__) && !defined(__ANDROID__) 371 | conf = _flipclock_open_conf_linux(app->conf_path); 372 | #endif 373 | // Should never happen, but it's fine. 374 | if (conf == NULL) 375 | return; 376 | #if !defined(__ANDROID__) 377 | LOG_DEBUG("Parsing `%s`.\n", app->conf_path); 378 | #endif /** 379 | * Most file systems have max file name length limit. 380 | * So I don't need to alloc memory dynamically. 381 | */ 382 | char conf_line[MAX_BUFFER_LENGTH]; 383 | char *key; 384 | char *value; 385 | while (fgets(conf_line, MAX_BUFFER_LENGTH, conf) != NULL) { 386 | if (strlen(conf_line) == MAX_BUFFER_LENGTH - 1) 387 | LOG_ERROR("`conf_line` too long, may fail to load.\n"); 388 | if (_flipclock_parse_key_value(conf_line, &key, &value)) 389 | continue; 390 | LOG_DEBUG("Parsed key `%s` and value `%s`.\n", key, value); 391 | _flipclock_apply_key_value(app, key, value); 392 | } 393 | fclose(conf); 394 | } 395 | 396 | static void _flipclock_create_clocks(struct flipclock *app) 397 | { 398 | RETURN_IF_FAIL(app != NULL); 399 | 400 | // Create window for each display if fullscreen. 401 | if (app->full) { 402 | /** 403 | * Instead of handling display number changing, 404 | * let user restart program is easier. 405 | */ 406 | app->clocks_length = SDL_GetNumVideoDisplays(); 407 | SDL_ShowCursor(SDL_DISABLE); 408 | } 409 | // I know what I am doing, silly tidy tools. 410 | // NOLINTNEXTLINE(bugprone-sizeof-expression) 411 | app->clocks = malloc(sizeof(*app->clocks) * app->clocks_length); 412 | if (app->clocks == NULL) { 413 | LOG_ERROR("Failed to create clocks!\n"); 414 | exit(EXIT_FAILURE); 415 | } 416 | for (int i = 0; i < app->clocks_length; ++i) 417 | app->clocks[i] = flipclock_clock_create(app, i); 418 | } 419 | 420 | #if defined(_WIN32) 421 | /** 422 | * There is another silly design in Windows screensaver chooser (yes, differs 423 | * from the one happens when you choose other screensaver and then choose 424 | * back!). When you press preview button to start a fullscreen screensaver and 425 | * close it, Windows will launch another process in the small preview window! 426 | * And even SDL_RENDER_TARGETS_RESET is not sent this time! So there is no 427 | * other way than creating lock files by ourselves, what a horrible system! 428 | */ 429 | 430 | // Have to use global variable here because of atexit(). 431 | char preview_lock_path[MAX_BUFFER_LENGTH] = { '\0' }; 432 | 433 | static void _flipclock_get_preview_lock_path_win32(HWND preview_window, 434 | const char program_dir[]) 435 | { 436 | RETURN_IF_FAIL(program_dir != NULL); 437 | 438 | /** 439 | * User can open more than screensaver chooser, 440 | * so we need to add HWND as part of lock file name. 441 | */ 442 | snprintf(preview_lock_path, MAX_BUFFER_LENGTH, "%s\\flipclock.%lu.lock", 443 | program_dir, preview_window); 444 | preview_lock_path[MAX_BUFFER_LENGTH - 1] = '\0'; 445 | if (strlen(preview_lock_path) == MAX_BUFFER_LENGTH - 1) 446 | LOG_ERROR("`preview_lock_path` too long, may fail to load.\n"); 447 | } 448 | 449 | static void _flipclock_remove_preview_lock_win32(void) 450 | { 451 | /** 452 | * UNIX is designed for normal people, because you can remove one 453 | * file after you just opened it, system will close it until your 454 | * program ends. While silly Windows just say "cannot modify an opened 455 | * file". So this function is used for atexit(), we have to remove 456 | * file by ourselves at exit. 457 | */ 458 | remove(preview_lock_path); 459 | } 460 | 461 | static void _flipclock_lock_preview_win32(struct flipclock *app) 462 | { 463 | RETURN_IF_FAIL(app != NULL); 464 | 465 | _flipclock_get_preview_lock_path_win32(app->preview_window, 466 | app->program_dir); 467 | LOG_DEBUG("Using `preview_lock_path` `%s`.\n", preview_lock_path); 468 | FILE *preview_lock = fopen(preview_lock_path, "r"); 469 | if (preview_lock != NULL || errno != ENOENT) { 470 | LOG_ERROR("Already running in the given preview window!\n"); 471 | fclose(preview_lock); 472 | exit(EXIT_FAILURE); 473 | } 474 | preview_lock = fopen(preview_lock_path, "w"); 475 | fprintf(preview_lock, "%lu\n", app->preview_window); 476 | fclose(preview_lock); 477 | atexit(_flipclock_remove_preview_lock_win32); 478 | } 479 | 480 | static void _flipclock_create_preview_win32(struct flipclock *app) 481 | { 482 | RETURN_IF_FAIL(app != NULL); 483 | 484 | _flipclock_lock_preview_win32(app); 485 | app->clocks = malloc(sizeof(*app->clocks) * app->clocks_length); 486 | if (app->clocks == NULL) { 487 | LOG_ERROR("Failed to create clocks!\n"); 488 | exit(EXIT_FAILURE); 489 | } 490 | // Create window from native window when in preview. 491 | app->clocks[0] = flipclock_clock_create_preview(app); 492 | } 493 | 494 | static void _flipclock_create_clocks_win32(struct flipclock *app) 495 | { 496 | RETURN_IF_FAIL(app != NULL); 497 | 498 | if (!app->screensaver) 499 | SDL_DisableScreenSaver(); 500 | if (app->preview) 501 | _flipclock_create_preview_win32(app); 502 | else 503 | _flipclock_create_clocks(app); 504 | } 505 | #endif 506 | 507 | void flipclock_create_clocks(struct flipclock *app) 508 | { 509 | RETURN_IF_FAIL(app != NULL); 510 | 511 | #if defined(_WIN32) 512 | _flipclock_create_clocks_win32(app); 513 | #else 514 | // Android and Linux should share the same code here. 515 | SDL_DisableScreenSaver(); 516 | _flipclock_create_clocks(app); 517 | #endif 518 | } 519 | 520 | static void _flipclock_set_show_second(struct flipclock *app, bool show_second) 521 | { 522 | RETURN_IF_FAIL(app != NULL); 523 | 524 | app->show_second = show_second; 525 | for (int i = 0; i < app->clocks_length; ++i) { 526 | if (app->clocks[i] == NULL) 527 | continue; 528 | flipclock_clock_set_show_second(app->clocks[i], show_second); 529 | } 530 | } 531 | 532 | static void _flipclock_set_fullscreen(struct flipclock *app, bool full) 533 | { 534 | RETURN_IF_FAIL(app != NULL); 535 | 536 | app->full = full; 537 | if (full) 538 | SDL_ShowCursor(SDL_DISABLE); 539 | else 540 | SDL_ShowCursor(SDL_ENABLE); 541 | for (int i = 0; i < app->clocks_length; ++i) { 542 | if (app->clocks[i] == NULL) 543 | continue; 544 | flipclock_clock_set_fullscreen(app->clocks[i], full); 545 | } 546 | } 547 | 548 | /** 549 | * If you changed `ampm`, you must call `_flipclock_set_hour()` after it, 550 | * because hour number will change in differet types. 551 | */ 552 | static void _flipclock_set_ampm(struct flipclock *app, bool ampm) 553 | { 554 | RETURN_IF_FAIL(app != NULL); 555 | 556 | app->ampm = ampm; 557 | if (app->ampm) { 558 | for (int i = 0; i < app->clocks_length; ++i) { 559 | if (app->clocks[i] == NULL) 560 | continue; 561 | char text[3]; 562 | snprintf(text, sizeof(text), "%cM", 563 | app->now.tm_hour / 12 ? 'P' : 'A'); 564 | flipclock_clock_set_ampm(app->clocks[i], text); 565 | } 566 | } else { 567 | for (int i = 0; i < app->clocks_length; ++i) { 568 | if (app->clocks[i] == NULL) 569 | continue; 570 | flipclock_clock_set_ampm(app->clocks[i], NULL); 571 | } 572 | } 573 | } 574 | 575 | static void _flipclock_set_hour(struct flipclock *app, bool flip) 576 | { 577 | RETURN_IF_FAIL(app != NULL); 578 | 579 | for (int i = 0; i < app->clocks_length; ++i) { 580 | if (app->clocks[i] == NULL) 581 | continue; 582 | char text[3]; 583 | strftime(text, sizeof(text), app->ampm ? "%I" : "%H", 584 | &app->now); 585 | // Trim zero when using 12-hour clock. 586 | if (app->ampm && text[0] == '0') { 587 | text[0] = text[1]; 588 | text[1] = text[2]; 589 | } 590 | flipclock_clock_set_hour(app->clocks[i], text, flip); 591 | } 592 | } 593 | 594 | static void _flipclock_set_minute(struct flipclock *app, bool flip) 595 | { 596 | RETURN_IF_FAIL(app != NULL); 597 | 598 | for (int i = 0; i < app->clocks_length; ++i) { 599 | if (app->clocks[i] == NULL) 600 | continue; 601 | char text[3]; 602 | strftime(text, sizeof(text), "%M", &app->now); 603 | flipclock_clock_set_minute(app->clocks[i], text, flip); 604 | } 605 | } 606 | 607 | static void _flipclock_set_second(struct flipclock *app, bool flip) 608 | { 609 | RETURN_IF_FAIL(app != NULL); 610 | 611 | for (int i = 0; i < app->clocks_length; ++i) { 612 | if (app->clocks[i] == NULL) 613 | continue; 614 | char text[3]; 615 | strftime(text, sizeof(text), "%S", &app->now); 616 | flipclock_clock_set_second(app->clocks[i], text, flip); 617 | } 618 | } 619 | 620 | static void _flipclock_animate(struct flipclock *app) 621 | { 622 | RETURN_IF_FAIL(app != NULL); 623 | 624 | // Pause when minimized. 625 | for (int i = 0; i < app->clocks_length; ++i) { 626 | if (app->clocks[i] == NULL) 627 | continue; 628 | if (!app->clocks[i]->waiting) 629 | flipclock_clock_animate(app->clocks[i]); 630 | } 631 | } 632 | 633 | static void _flipclock_handle_window_event(struct flipclock *app, 634 | SDL_Event event) 635 | { 636 | RETURN_IF_FAIL(app != NULL); 637 | 638 | struct flipclock_clock *clock = NULL; 639 | for (int i = 0; i < app->clocks_length; ++i) { 640 | // Ignore closed clocks. 641 | if (app->clocks[i] == NULL) 642 | continue; 643 | if (event.window.windowID == 644 | SDL_GetWindowID(app->clocks[i]->window)) { 645 | clock = app->clocks[i]; 646 | break; 647 | } 648 | } 649 | if (clock == NULL) { 650 | LOG_ERROR("There is no running window that event belongs!\n"); 651 | // It should be safe to ignore this event. 652 | return; 653 | } 654 | flipclock_clock_handle_window_event(clock, event); 655 | } 656 | 657 | static void _flipclock_handle_keydown(struct flipclock *app, SDL_Event event) 658 | { 659 | RETURN_IF_FAIL(app != NULL); 660 | 661 | switch (event.key.keysym.sym) { 662 | case SDLK_ESCAPE: 663 | case SDLK_q: 664 | case SDLK_AC_BACK: 665 | app->running = false; 666 | break; 667 | case SDLK_t: 668 | LOG_DEBUG("Key `t` pressed.\n"); 669 | _flipclock_set_ampm(app, !app->ampm); 670 | _flipclock_set_hour(app, false); 671 | break; 672 | case SDLK_f: 673 | LOG_DEBUG("Key `f` pressed.\n"); 674 | _flipclock_set_fullscreen(app, !app->full); 675 | break; 676 | case SDLK_s: 677 | LOG_DEBUG("Key `s` pressed.\n"); 678 | _flipclock_set_show_second(app, !app->show_second); 679 | // Must set second text, because created card has empty text. 680 | _flipclock_set_second(app, false); 681 | break; 682 | default: 683 | break; 684 | } 685 | } 686 | 687 | static void _flipclock_handle_event(struct flipclock *app, SDL_Event event) 688 | { 689 | RETURN_IF_FAIL(app != NULL); 690 | 691 | switch (event.type) { 692 | #if defined(_WIN32) 693 | /** 694 | * There is a silly design in Windows screensaver 695 | * chooser. When you choose one screensaver, it will 696 | * run the program with `/p HWND`, but if you changed 697 | * to another, the former will not receive close 698 | * event (yes, any kind of close event is not sent), 699 | * and if you choose the former again, it will run 700 | * the program with the same HWND again! And your 701 | * previous program only get a SDL_RENDER_TARGETS_RESET. 702 | * So we have to close the lost program manually here. 703 | */ 704 | case SDL_RENDER_TARGETS_RESET: 705 | if (app->preview) 706 | app->running = false; 707 | break; 708 | #endif 709 | case SDL_WINDOWEVENT: 710 | _flipclock_handle_window_event(app, event); 711 | break; 712 | #if defined(_WIN32) 713 | /** 714 | * If under Windows, and not in preview window, 715 | * and it was called as a screensaver, 716 | * just exit when user press mouse button or move it, 717 | * or interactive with touch screen. 718 | */ 719 | case SDL_MOUSEBUTTONDOWN: 720 | case SDL_MOUSEMOTION: 721 | case SDL_MOUSEWHEEL: 722 | if (!app->preview && app->screensaver) 723 | app->running = false; 724 | break; 725 | #endif 726 | case SDL_FINGERDOWN: 727 | #if defined(_WIN32) 728 | if (!app->preview && app->screensaver) 729 | app->running = false; 730 | break; 731 | #else 732 | // TODO: May not work, 3 fingers contains 2 fingers. 733 | switch (SDL_GetNumTouchFingers(event.tfinger.touchId)) { 734 | case 2: 735 | LOG_DEBUG("2 finger touch detected!\n"); 736 | _flipclock_set_ampm(app, !app->ampm); 737 | // Setting ampm always changes hour. 738 | _flipclock_set_hour(app, false); 739 | break; 740 | case 3: 741 | LOG_DEBUG("3 finger touch detected!\n"); 742 | _flipclock_set_show_second(app, !app->show_second); 743 | /** 744 | * Must set second text, because created card has empty 745 | * text. 746 | */ 747 | _flipclock_set_second(app, false); 748 | break; 749 | default: 750 | break; 751 | } 752 | break; 753 | #endif 754 | 755 | /** 756 | * For touch devices, the most used function is 757 | * changing type, so we use double tap for it, 758 | * instead of toggling fullscreen. 759 | * Double tap (less then 300ms) changes type. 760 | */ 761 | case SDL_FINGERUP: 762 | if (event.tfinger.fingerId == app->last_touch_finger && 763 | event.tfinger.timestamp < 764 | app->last_touch_time + DOUBLE_TAP_INTERVAL_MS) { 765 | LOG_DEBUG("Double tap detected.\n"); 766 | _flipclock_set_ampm(app, !app->ampm); 767 | _flipclock_set_hour(app, false); 768 | } 769 | app->last_touch_time = event.tfinger.timestamp; 770 | app->last_touch_finger = event.tfinger.fingerId; 771 | break; 772 | case SDL_KEYDOWN: 773 | #if defined(_WIN32) 774 | /** 775 | * If under Windows, and not in preview window, 776 | * and it was called as a screensaver. 777 | * just exit when user press any key. 778 | * But if it was not called as a screensaver, 779 | * it only handles some special keys. 780 | * Also, we do nothing when in preview. 781 | */ 782 | if (!app->preview && app->screensaver) 783 | app->running = false; 784 | else if (!app->preview) 785 | _flipclock_handle_keydown(app, event); 786 | #else 787 | // It's simple under Linux and Android. 788 | _flipclock_handle_keydown(app, event); 789 | #endif 790 | break; 791 | case SDL_QUIT: 792 | app->running = false; 793 | break; 794 | default: 795 | break; 796 | } 797 | } 798 | 799 | void flipclock_run_mainloop(struct flipclock *app) 800 | { 801 | RETURN_IF_FAIL(app != NULL); 802 | 803 | SDL_Event event; 804 | // Clear event queue before running. 805 | while (SDL_PollEvent(&event)) 806 | ; 807 | // First frame when app starts. 808 | _flipclock_set_ampm(app, app->ampm); 809 | _flipclock_set_hour(app, false); 810 | _flipclock_set_minute(app, false); 811 | if (app->show_second) 812 | _flipclock_set_second(app, false); 813 | _flipclock_animate(app); 814 | while (app->running) { 815 | #if defined(_WIN32) 816 | // Exit when preview window closed. 817 | if (app->preview && !IsWindow(app->preview_window)) 818 | app->running = false; 819 | #endif 820 | if (SDL_WaitEventTimeout(&event, 1000 / FPS)) 821 | _flipclock_handle_event(app, event); 822 | struct tm past = app->now; 823 | time_t raw_time = time(NULL); 824 | app->now = *localtime(&raw_time); 825 | if (app->now.tm_hour != past.tm_hour) { 826 | _flipclock_set_ampm(app, app->ampm); 827 | _flipclock_set_hour(app, true); 828 | } 829 | if (app->now.tm_min != past.tm_min) 830 | _flipclock_set_minute(app, true); 831 | if (app->show_second && app->now.tm_sec != past.tm_sec) 832 | _flipclock_set_second(app, true); 833 | _flipclock_animate(app); 834 | } 835 | } 836 | 837 | void flipclock_destroy_clocks(struct flipclock *app) 838 | { 839 | RETURN_IF_FAIL(app != NULL); 840 | 841 | for (int i = 0; i < app->clocks_length; ++i) { 842 | if (app->clocks[i] == NULL) 843 | continue; 844 | flipclock_clock_destroy(app->clocks[i]); 845 | } 846 | free(app->clocks); 847 | if (app->full) 848 | SDL_ShowCursor(SDL_ENABLE); 849 | #if defined(_WIN32) 850 | if (!app->screensaver) 851 | SDL_EnableScreenSaver(); 852 | #else 853 | SDL_EnableScreenSaver(); 854 | #endif 855 | } 856 | 857 | void flipclock_destroy(struct flipclock *app) 858 | { 859 | RETURN_IF_FAIL(app != NULL); 860 | 861 | free(app); 862 | } 863 | 864 | void flipclock_print_help(struct flipclock *app, char program_name[]) 865 | { 866 | RETURN_IF_FAIL(app != NULL); 867 | RETURN_IF_FAIL(program_name != NULL); 868 | 869 | printf("A simple flip clock screensaver using SDL2.\n"); 870 | #if !defined(__ANDROID__) 871 | printf("Version " PROJECT_VERSION ".\n"); 872 | #endif 873 | printf("Usage: %s [OPTION...] \n", program_name); 874 | printf("Options:\n"); 875 | printf("\t%ch\t\tDisplay help then exit.\n", OPT_START); 876 | printf("\t%cv\t\tDisplay version then exit.\n", OPT_START); 877 | #if defined(_WIN32) 878 | printf("\t%cs\t\t(Windows only) " 879 | "Required for starting screensaver in Windows.\n", 880 | OPT_START); 881 | printf("\t%cc\t\t(Windows only) Dummy configuration dialog.\n", 882 | OPT_START); 883 | printf("\t%cp \t(Windows only) Preview in given window.\n", 884 | OPT_START); 885 | #endif 886 | printf("\t%c3\t\tShow second.\n", OPT_START); 887 | printf("\t%cw\t\tRun as a window instead of fullscreen.\n", OPT_START); 888 | printf("\t%ct <12|24>\tToggle 12-hour clock format (AM/PM) " 889 | "or 24-hour clock format.\n", 890 | OPT_START); 891 | printf("\t%cf \tLoad custom font path.\n", OPT_START); 892 | printf("Press Esc or q to exit.\n"); 893 | printf("Press s to toggle second.\n"); 894 | printf("Press f to toggle fullscreen.\n"); 895 | printf("Press t to toggle 12/24-hour clock format.\n"); 896 | printf("Using configuration file %s.\n", app->conf_path); 897 | } 898 | --------------------------------------------------------------------------------