├── .gitignore ├── .travis.yml ├── LICENSE ├── README.rst ├── assets ├── android │ └── template │ │ ├── .gradle │ │ ├── 3.3 │ │ │ └── taskArtifacts │ │ │ │ ├── fileHashes.bin │ │ │ │ ├── fileSnapshots.bin │ │ │ │ ├── taskArtifacts.bin │ │ │ │ └── taskArtifacts.lock │ │ ├── 4.10.2 │ │ │ ├── fileChanges │ │ │ │ └── last-build.bin │ │ │ ├── fileHashes │ │ │ │ ├── fileHashes.bin │ │ │ │ └── fileHashes.lock │ │ │ └── gc.properties │ │ ├── 7.5 │ │ │ ├── checksums │ │ │ │ └── checksums.lock │ │ │ ├── dependencies-accessors │ │ │ │ ├── dependencies-accessors.lock │ │ │ │ └── gc.properties │ │ │ ├── fileChanges │ │ │ │ └── last-build.bin │ │ │ ├── fileHashes │ │ │ │ └── fileHashes.lock │ │ │ └── gc.properties │ │ └── vcs-1 │ │ │ └── gc.properties │ │ ├── build.gradle │ │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ │ ├── gradlew │ │ ├── gradlew.bat │ │ ├── jni │ │ ├── Android.mk │ │ ├── Application.mk │ │ ├── main │ │ │ └── Android.mk │ │ └── src │ │ │ ├── Android.mk │ │ │ └── Android_static.mk │ │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── io │ │ │ └── github │ │ │ └── yglukhov │ │ │ └── nimx │ │ │ └── NimxActivity.java │ │ └── res │ │ └── values │ │ └── strings.xml └── main.html ├── deployGHPages.sh ├── doc ├── README.md ├── architecture-overview.md ├── building-for-different-platforms.md ├── hello-world-app.md ├── hello-world-app.png ├── index.rst ├── layout-dsl.md ├── nimdoc.cfg ├── sample-screenshot.png ├── views.md └── what-is-nimx.md ├── editor ├── nakefile.nim ├── nimxedit.nim └── res │ └── assets │ ├── back.nimx │ └── top_panel.nimx ├── nakefile.nim ├── nimx.nimble ├── nimx ├── abstract_window.nim ├── all_views.nim ├── animation.nim ├── animation_runner.nim ├── app.nim ├── assets │ ├── abstract_asset_bundle.nim │ ├── android_asset_bundle.nim │ ├── android_asset_url_handler.nim │ ├── asset_cache.nim │ ├── asset_loader.nim │ ├── asset_loading.nim │ ├── asset_manager.nim │ ├── json_loading.nim │ ├── native_asset_bundle.nim │ ├── url_stream.nim │ ├── web_asset_bundle.nim │ └── web_url_handler.nim ├── autotest.nim ├── button.nim ├── class_registry.nim ├── clip_view.nim ├── collection_view.nim ├── color.nim ├── color_picker.nim ├── common │ └── expand_button.nim ├── composition.nim ├── context.nim ├── control.nim ├── cursor.nim ├── drag_and_drop.nim ├── editor │ ├── bezier_view.nim │ ├── edit_view.nim │ ├── editor_types.nim │ ├── editor_workspace.nim │ ├── grid_drawing.nim │ ├── tab_view.nim │ └── ui_document.nim ├── event.nim ├── expanding_view.nim ├── font.nim ├── form_view.nim ├── formatted_text.nim ├── gesture_detector.nim ├── gesture_detector_newtouch.nim ├── horizontal_list_view.nim ├── http_request.nim ├── image.nim ├── image_preview.nim ├── image_view.nim ├── inspector_panel.nim ├── inspector_view.nim ├── key_commands.nim ├── keyboard.nim ├── layout.nim ├── layout_vars.nim ├── linear_layout.nim ├── linkage_details.nim ├── load_image_impl.nim ├── matrixes.nim ├── menu.nim ├── meta_extensions │ ├── property_desc.nim │ ├── serializers_gen.nim │ └── visitors_gen.nim ├── mini_profiler.nim ├── naketools.nim ├── notification_center.nim ├── numeric_text_field.nim ├── outline_view.nim ├── panel_view.nim ├── pasteboard │ ├── abstract_pasteboard.nim │ ├── pasteboard.nim │ ├── pasteboard_item.nim │ ├── pasteboard_mac.nim │ ├── pasteboard_web.nim │ ├── pasteboard_win.nim │ └── pasteboard_x11.nim ├── pathutils.nim ├── perform_on_main_thread.nim ├── popup_button.nim ├── portable_gl.nim ├── private │ ├── async.nim │ ├── font │ │ ├── font_data.nim │ │ ├── fontconfig.nim │ │ ├── js_glyph_provider.nim │ │ ├── stb_ttf_glyph_provider.nim │ │ └── web_glyph_provider.nim │ ├── helper_macros.nim │ ├── image_pvr.nim │ ├── js_data_view_stream.nim │ ├── js_platform_detector.nim │ ├── js_vk_map.nim │ ├── kiwi_vector_symbolics.nim │ ├── objc_appkit.nim │ ├── sdl_vk_map.nim │ ├── simple_table.nim │ ├── text_drawing.nim │ ├── winapi_vk_map.nim │ ├── windows │ │ ├── appkit_window.nim │ │ ├── emscripten_window.nim │ │ ├── js_canvas_window.nim │ │ ├── sdl_window.nim │ │ ├── web_window.nim │ │ ├── winapi_window.nim │ │ └── x11_window.nim │ ├── worker_queue.nim │ └── x11_vk_map.nim ├── progress_indicator.nim ├── property_editors │ ├── autoresizing_mask_editor.nim │ ├── propedit_registry.nim │ └── standard_editors.nim ├── property_visitor.nim ├── rect_packer.nim ├── render_to_image.nim ├── screen.nim ├── scroll_bar.nim ├── scroll_view.nim ├── segmented_control.nim ├── serializers.nim ├── slider.nim ├── split_view.nim ├── stack_view.nim ├── system_logger.nim ├── table_view.nim ├── table_view2.nim ├── table_view_cell.nim ├── text_field.nim ├── timer.nim ├── toolbar.nim ├── types.nim ├── ui_resource.nim ├── undo_manager.nim ├── unistring.nim ├── utils │ ├── android.nim │ └── lower_bound.nim ├── view.nim ├── view_dragging_listener.nim ├── view_event_handling.nim ├── view_event_handling_new.nim ├── view_render_to_image.nim ├── window.nim ├── window_event_handling.nim └── write_image_impl.nim └── test ├── main.nim ├── nakefile.nim ├── res ├── Base.lproj │ └── LaunchScreen.nib ├── Info.plist ├── MyGame.ico ├── OpenSans-Regular.ttf ├── PkgInfo ├── cat.jpg └── tile.png ├── sample01_welcome.nim ├── sample02_controls.nim ├── sample03_image.nim ├── sample04_animation.nim ├── sample05_fonts.nim ├── sample06_timers.nim ├── sample07_collections.nim ├── sample08_events.nim ├── sample09_docking_tabs.nim ├── sample10_text.nim ├── sample11_expanded_views.nim ├── sample12_menus.nim ├── sample13_drag_and_drop.nim ├── sample14_layout.nim ├── sample15_animation_easings.nim ├── sample16_outline.nim └── sample_registry.nim /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache 2 | nakefile 3 | test/main 4 | *.exe 5 | build/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | services: 3 | - docker 4 | before_install: 5 | - docker pull yglukhov/nim-ui-base 6 | script: 7 | - docker run -v "$(pwd):/project" -w /project yglukhov/nim-ui-base run "nimble install -y && nake tests && nake samples -d:js --norun && nake docs" && ./deployGHPages.sh ./build/doc 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Yuriy Glukhov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | nimx |travis| |nimble| 3 | =========== 4 | 5 | .. |travis| image:: https://travis-ci.org/yglukhov/nimx.svg?branch=master 6 | :target: https://travis-ci.org/yglukhov/nimx 7 | 8 | .. |nimble| image:: https://raw.githubusercontent.com/yglukhov/nimble-tag/master/nimble_js.png 9 | :target: https://github.com/yglukhov/nimble-tag 10 | 11 | Cross-platform GUI framework in `Nim `_. 12 | 13 | `Live demo in WebGL `_ 14 | 15 | 16 | .. image:: ./doc/sample-screenshot.png 17 | 18 | Usage 19 | ------------ 20 | .. code-block:: nim 21 | 22 | # File: main.nim 23 | import nimx/window 24 | import nimx/text_field 25 | 26 | proc startApp() = 27 | # First create a window. Window is the root of view hierarchy. 28 | var wnd = newWindow(newRect(40, 40, 800, 600)) 29 | 30 | # Create a static text field and add it to view hierarchy 31 | let label = newLabel(newRect(20, 20, 150, 20)) 32 | label.text = "Hello, world!" 33 | wnd.addSubview(label) 34 | 35 | # Run the app 36 | runApplication: 37 | startApp() 38 | 39 | Running 40 | ------------ 41 | 42 | .. code-block:: sh 43 | 44 | nim c -r --threads:on main.nim 45 | 46 | Supported target platforms 47 | ------------ 48 | Nimx officially supports Linux, MacOS, Windows, Android, iOS and WebAssembly (with Nim C backend and `Emscripten `_, and Emscripten-less compilation is experimental). 49 | 50 | Troubleshooting 51 | ------------ 52 | Nimx is tested only against the latest devel version of Nim compiler. Before reporting any issues please verify that your Nim is as fresh as possible. 53 | 54 | Running nimx samples 55 | ==================== 56 | .. code-block:: sh 57 | 58 | git clone https://github.com/yglukhov/nimx 59 | cd nimx 60 | nimble install -dy 61 | nake # Build and run on the current platform 62 | # or 63 | nake js # Build and run in default web browser 64 | 65 | Reference 66 | ==================== 67 | See `the docs <./doc>`_ for more information. 68 | -------------------------------------------------------------------------------- /assets/android/template/.gradle/3.3/taskArtifacts/fileHashes.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/nimx/d93a6795a7e473627e52763deee0cb2df2d1a7fc/assets/android/template/.gradle/3.3/taskArtifacts/fileHashes.bin -------------------------------------------------------------------------------- /assets/android/template/.gradle/3.3/taskArtifacts/fileSnapshots.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/nimx/d93a6795a7e473627e52763deee0cb2df2d1a7fc/assets/android/template/.gradle/3.3/taskArtifacts/fileSnapshots.bin -------------------------------------------------------------------------------- /assets/android/template/.gradle/3.3/taskArtifacts/taskArtifacts.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/nimx/d93a6795a7e473627e52763deee0cb2df2d1a7fc/assets/android/template/.gradle/3.3/taskArtifacts/taskArtifacts.bin -------------------------------------------------------------------------------- /assets/android/template/.gradle/3.3/taskArtifacts/taskArtifacts.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/nimx/d93a6795a7e473627e52763deee0cb2df2d1a7fc/assets/android/template/.gradle/3.3/taskArtifacts/taskArtifacts.lock -------------------------------------------------------------------------------- /assets/android/template/.gradle/4.10.2/fileChanges/last-build.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/android/template/.gradle/4.10.2/fileHashes/fileHashes.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/nimx/d93a6795a7e473627e52763deee0cb2df2d1a7fc/assets/android/template/.gradle/4.10.2/fileHashes/fileHashes.bin -------------------------------------------------------------------------------- /assets/android/template/.gradle/4.10.2/fileHashes/fileHashes.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/nimx/d93a6795a7e473627e52763deee0cb2df2d1a7fc/assets/android/template/.gradle/4.10.2/fileHashes/fileHashes.lock -------------------------------------------------------------------------------- /assets/android/template/.gradle/4.10.2/gc.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/nimx/d93a6795a7e473627e52763deee0cb2df2d1a7fc/assets/android/template/.gradle/4.10.2/gc.properties -------------------------------------------------------------------------------- /assets/android/template/.gradle/7.5/checksums/checksums.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/nimx/d93a6795a7e473627e52763deee0cb2df2d1a7fc/assets/android/template/.gradle/7.5/checksums/checksums.lock -------------------------------------------------------------------------------- /assets/android/template/.gradle/7.5/dependencies-accessors/dependencies-accessors.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/nimx/d93a6795a7e473627e52763deee0cb2df2d1a7fc/assets/android/template/.gradle/7.5/dependencies-accessors/dependencies-accessors.lock -------------------------------------------------------------------------------- /assets/android/template/.gradle/7.5/dependencies-accessors/gc.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/nimx/d93a6795a7e473627e52763deee0cb2df2d1a7fc/assets/android/template/.gradle/7.5/dependencies-accessors/gc.properties -------------------------------------------------------------------------------- /assets/android/template/.gradle/7.5/fileChanges/last-build.bin: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /assets/android/template/.gradle/7.5/fileHashes/fileHashes.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/nimx/d93a6795a7e473627e52763deee0cb2df2d1a7fc/assets/android/template/.gradle/7.5/fileHashes/fileHashes.lock -------------------------------------------------------------------------------- /assets/android/template/.gradle/7.5/gc.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/nimx/d93a6795a7e473627e52763deee0cb2df2d1a7fc/assets/android/template/.gradle/7.5/gc.properties -------------------------------------------------------------------------------- /assets/android/template/.gradle/vcs-1/gc.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/nimx/d93a6795a7e473627e52763deee0cb2df2d1a7fc/assets/android/template/.gradle/vcs-1/gc.properties -------------------------------------------------------------------------------- /assets/android/template/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | // maven { 6 | // url 'https://mvnrepository.com/artifact/com.android.tools.lint/lint-gradle-api' 7 | // } 8 | } 9 | 10 | dependencies { 11 | classpath("com.android.tools.build:gradle:7.4.0") 12 | // classpath 'com.android.tools.lint:lint-gradle:26.1.3' 13 | } 14 | } 15 | 16 | allprojects{ 17 | repositories { 18 | google() 19 | mavenCentral() 20 | } 21 | } 22 | 23 | apply plugin: 'com.android.application' 24 | 25 | android { 26 | compileSdkVersion 26 27 | namespace = "$(PACKAGE_ID)" 28 | 29 | defaultConfig { 30 | ndk { 31 | // We get targetArchitectures from names of dirs in $projectDir/jni/src, as that's where naketools put nimcache 32 | def targetArchitectures = file("$projectDir/jni/src").listFiles().findAll { it.isDirectory() }.collect { it.name } 33 | abiFilters = targetArchitectures 34 | } 35 | 36 | externalNativeBuild { 37 | ndkBuild { 38 | arguments "-j8" 39 | if (org.gradle.internal.os.OperatingSystem.current().isWindows()) { 40 | arguments "LOCAL_SHORT_COMMANDS=true" 41 | } 42 | } 43 | } 44 | 45 | minSdk $(TARGET_API) 46 | } 47 | 48 | externalNativeBuild { 49 | ndkBuild { 50 | path "jni/Android.mk" 51 | } 52 | } 53 | 54 | buildTypes { 55 | debug { 56 | jniDebuggable true 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /assets/android/template/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/nimx/d93a6795a7e473627e52763deee0cb2df2d1a7fc/assets/android/template/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /assets/android/template/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /assets/android/template/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /assets/android/template/jni/Android.mk: -------------------------------------------------------------------------------- 1 | include $(call all-subdir-makefiles) -------------------------------------------------------------------------------- /assets/android/template/jni/Application.mk: -------------------------------------------------------------------------------- 1 | 2 | # Uncomment this if you're using STL in your project 3 | # See CPLUSPLUS-SUPPORT.html in the NDK documentation for more information 4 | # APP_STL := stlport_static 5 | -------------------------------------------------------------------------------- /assets/android/template/jni/main/Android.mk: -------------------------------------------------------------------------------- 1 | LOCAL_PATH := $(call my-dir) 2 | 3 | include $(CLEAR_VARS) 4 | LOCAL_MODULE := main_static 5 | LOCAL_SRC_FILES := $(TARGET_ARCH_ABI)/libmain_static.a 6 | LOCAL_LD_LIBS := -latomic -lc 7 | LOCAL_EXPORT_LDLIBS := -lc 8 | LOCAL_CFLAGS := -latomic -lc 9 | include $(PREBUILT_STATIC_LIBRARY) 10 | -------------------------------------------------------------------------------- /assets/android/template/jni/src/Android.mk: -------------------------------------------------------------------------------- 1 | LOCAL_PATH := $(call my-dir) 2 | 3 | include $(CLEAR_VARS) 4 | 5 | LOCAL_MODULE := main 6 | 7 | SDL_PATH := ../SDL 8 | 9 | LOCAL_C_INCLUDES := $(LOCAL_PATH)/$(SDL_PATH)/include $(NIM_INCLUDE_DIR) 10 | 11 | ABI_SRC_PATH := $(LOCAL_PATH)/$(APP_ABI) 12 | 13 | # Add your application source files here... 14 | LOCAL_SRC_FILES := $(SDL_PATH)/src/main/android/SDL_android_main.c \ 15 | $(patsubst $(ABI_SRC_PATH)/%, $(APP_ABI)/%, $(wildcard $(ABI_SRC_PATH)/*.cpp)) \ 16 | $(patsubst $(ABI_SRC_PATH)/%, $(APP_ABI)/%, $(wildcard $(ABI_SRC_PATH)/*.c)) 17 | 18 | LOCAL_STATIC_LIBRARIES := $(STATIC_LIBRARIES) SDL2_static 19 | LOCAL_WHOLE_STATIC_LIBRARIES := main_static 20 | 21 | LOCAL_LDLIBS := -lGLESv1_CM -lGLESv2 -llog -landroid -lOpenSLES -lc $(ADDITIONAL_LINKER_FLAGS) 22 | LOCAL_CFLAGS := $(ADDITIONAL_COMPILER_FLAGS) -latomic -lc 23 | 24 | include $(BUILD_SHARED_LIBRARY) 25 | -------------------------------------------------------------------------------- /assets/android/template/jni/src/Android_static.mk: -------------------------------------------------------------------------------- 1 | # TODO: This file likely doesn't work. And shouldn't be used 2 | 3 | LOCAL_PATH := $(call my-dir) 4 | 5 | include $(CLEAR_VARS) 6 | 7 | LOCAL_MODULE := main 8 | 9 | LOCAL_SRC_FILES := $(patsubst $(LOCAL_PATH)/%, %, $(wildcard $(LOCAL_PATH)/src/*.cpp)) \ 10 | $(patsubst $(LOCAL_PATH)/%, %, $(wildcard $(LOCAL_PATH)/src/*.c)) 11 | 12 | LOCAL_STATIC_LIBRARIES := SDL2_static main_static 13 | 14 | include $(BUILD_SHARED_LIBRARY) 15 | $(call import-module,SDL)LOCAL_PATH := $(call my-dir) 16 | -------------------------------------------------------------------------------- /assets/android/template/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /assets/android/template/src/main/java/io/github/yglukhov/nimx/NimxActivity.java: -------------------------------------------------------------------------------- 1 | package io.github.yglukhov.nimx; 2 | import org.libsdl.app.SDLActivity; 3 | 4 | public class NimxActivity extends SDLActivity { 5 | 6 | @Override 7 | // For disabling include of dynamic SDL library file. 8 | // It's statically linked now. 9 | protected String[] getLibraries() { 10 | return new String[] {"main"}; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /assets/android/template/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(APP_NAME) 4 | 5 | -------------------------------------------------------------------------------- /assets/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /deployGHPages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This script requires GH_KEY environment variable to be set to Github access 4 | # token. The token may be generated at https://github.com/settings/tokens and 5 | # has to have permissions to commit (e.g. public_repo) 6 | 7 | # The variable itself then has to be set in travis-ci project settings. 8 | 9 | if [ "$GH_KEY" \!= "" ] 10 | then 11 | export GIT_DIR=/tmp/gh-pages.tmp 12 | rm -rf "$GIT_DIR" 13 | mkdir -p "$GIT_DIR" 14 | 15 | cd "$1" 16 | export GIT_WORK_TREE=$(pwd) 17 | git init 18 | 19 | git config user.name "Travis CI" 20 | git config user.email "autodocgen@example.com" 21 | 22 | git add . 23 | 24 | git commit -m "Deploy to GitHub Pages" 25 | 26 | git push --force --quiet "https://$GH_KEY@github.com/$TRAVIS_REPO_SLUG" master:gh-pages > /dev/null 2>&1 27 | fi 28 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | TOC 2 | === 3 | 4 | - Getting started 5 | - [What is nimx](what-is-nimx.md) 6 | - [Hello world app](hello-world-app.md) 7 | - [Building for different platforms](building-for-different-platforms.md) 8 | 9 | - Windows 10 | - Application 11 | 12 | - Layout 13 | - [Layout DSL](layout-dsl.md) 14 | - Linear Layouts 15 | - Constraint system 16 | 17 | - Controls 18 | - Label 19 | - [Buttons](../nimx/button.nim) 20 | - Button 21 | - ImageButton 22 | - Popup Button 23 | - CheckBox 24 | - RadioBox 25 | - Menus 26 | - Progress Indicator 27 | - Scrollbar 28 | - Segmented Control 29 | - Slider 30 | - Text Field 31 | - Label 32 | - Numeric 33 | - Text 34 | - Toolbar 35 | 36 | - [Views](views.md) 37 | - [Collection View](views.md#Collection-View) 38 | - [Expanding View](views.md#Expanding-View) 39 | - [Form View](views.md#Form-View) 40 | - [Horizontal List View](views.md#Horizontal-List-View) 41 | - [Image View](views.md#Image-View) 42 | - [Inspector View](views.md#Inspector-View) 43 | - [Outline View](views.md#Outline-View) 44 | - [Panel (collapsable view)](views.md#Panel-View) 45 | - Inspector Panel 46 | - [Scroll View](views.md#Scroll-View) 47 | - [Split View](views.md#Split-View) 48 | - [Stack View](views.md#Stack-View) 49 | - [Table View](views.md#Table-View) 50 | 51 | - Asset management 52 | 53 | - Custom views/controls 54 | - Drawing 55 | - Animation 56 | - Image 57 | - Image 58 | - Image Preview 59 | - Color 60 | - Composition 61 | - Context 62 | - Formatted Text 63 | - Screenshot 64 | - [Drag and Drop](../test/sample13_drag_and_drop.nim) 65 | - Event handling 66 | - Pasteboards 67 | - Timer 68 | - Updating controls (setNeedsDisplay() or update()) 69 | 70 | - Miscellaneous 71 | - [Cursors](../nimx/cursor.nim) 72 | - [Fonts](../nimx/font.nim) 73 | - Gestures 74 | - [HTTP Request](../nimx/http_request.nim) 75 | - [Key Commands](../nimx/key_commands.nim) 76 | - [Keyboard](../nimx/keyboard.nim) 77 | - [Paths](../nimx/pathutils.nim) 78 | - [Property Visitor](../nimx/property_visitor.nim) 79 | - [System Logger](../nimx/system_logger.nim) 80 | - [Unicode strings](../nimx/unistring.nim) 81 | - [Undo Manager](../nimx/undo_manager.nim) 82 | -------------------------------------------------------------------------------- /doc/architecture-overview.md: -------------------------------------------------------------------------------- 1 | Architecture overview 2 | ===================== 3 | 4 | Views 5 | ----- 6 | 7 | Everything that an app presents with nimx is organized in views. The base 8 | type of all views is the `View`. All other controls, such as buttons, textfields, 9 | tables inherit from it. 10 | 11 | Window 12 | ------ 13 | 14 | `Window` type also inherits from `View` and represents 15 | the actual OS window. For web platforms `Window` will represent a canvas on the 16 | document. `Window` should be inherited from only if you wish to support another 17 | platform. 18 | 19 | Hiearchy 20 | -------- 21 | 22 | The views in the window are organized in a hiararchy. E.g. a `Button` in a 23 | `Window` is one of the *subviews* of that `Window`, and the `Window` is the 24 | *superview* of the `Button`. 25 | 26 | Layout 27 | ------- 28 | 29 | The layout is defined by defining constraints between the views. Nimx provides 30 | its [layout DSL](layout-dsl.md) to make it easier. 31 | 32 | -------------------------------------------------------------------------------- /doc/building-for-different-platforms.md: -------------------------------------------------------------------------------- 1 | Building for different platforms 2 | ================================ 3 | 4 | 1. Create a `nakefile.nim` in the root of your project with the following contents: 5 | ```nim 6 | import nimx/naketools 7 | # You can add build configuration later 8 | ``` 9 | Here we assume that your main file is called `main.nim` and is located in the root of the project. Next, use `nake` to build. 10 | 11 | Nake usage 12 | ----------------- 13 | Flags: 14 | * `-d:release` - build in release mode 15 | * `--norun` - don't run the project after build 16 | 17 | Targets: 18 | * `ios` - iOS 19 | * `ios-sim` - iOS simulator 20 | * `droid` - Android 21 | * `js` - Nim JS backend 22 | * `emscripten` - Emscripten + Asm.js 23 | * `wasm` - Emscripten + Wasm 24 | * else - build for current desktop platform (Linux, MacOS, Windows) 25 | 26 | Examples: 27 | ```sh 28 | nake # Build and run for current platform 29 | nake --norun # Build for current platform 30 | nake -d:release droid # Build in release mode and run on currently connected device 31 | nake js # Build in debug mode and run in default browser 32 | ``` 33 | -------------------------------------------------------------------------------- /doc/hello-world-app.md: -------------------------------------------------------------------------------- 1 | Hello World App 2 | =============== 3 | 4 | Prerequisites 5 | ------------- 6 | 7 | Currently nimx uses [SDL2](https://www.libsdl.org/download-2.0.php) for all platforms except web, so make sure SDL2 library is installed and linkable. 8 | 9 | Install nimx 10 | ------------ 11 | Nimx is a regular [nimble](https://github.com/nim-lang/nimble) package so you could either install it or add as a dependecy to your project. 12 | ```sh 13 | # If you want to install nimx 14 | nimble install -y nimx 15 | 16 | # Or you could add it as your project dependency 17 | echo 'requires "nimx"' >> MYPROJECT.nimble 18 | # And tell nimble to update your project dependencies 19 | nimble install -dy 20 | ``` 21 | 22 | Hello World! 23 | ------------ 24 | 25 | Create a file `helloworld.nim` with the following contents: 26 | ```nim 27 | import nimx / [ window, layout, button, text_field ] 28 | 29 | runApplication: 30 | let w = newWindow(newRect(50, 50, 500, 150)) 31 | w.makeLayout: # DSL follows 32 | - Label as greetingLabel: # Add a view of type Label to the window. Create a local reference to it named greetingLabel. 33 | center == super # center point of the label should be equal to center point of superview 34 | width == 300 # width should be 300 points 35 | height == 15 # well, this should be obvious now 36 | text: "Press the Greet button to see the greeting" # property "text" should be set to whatever the label should display 37 | - Button: # Add a view of type Button. We're not referring to it so it's anonymous. 38 | centerX == super # center horizontally 39 | top == prev.bottom + 5 # the button should be lower than the label by 5 points 40 | width == 100 41 | height == 25 42 | title: "Greet" 43 | onAction: 44 | greetingLabel.text = "Hello, world!" 45 | ``` 46 | 47 | Compile and run: 48 | ```sh 49 | nim c -r --threads:on helloworld 50 | ``` 51 | 52 | The application should launch and look like so: 53 | ![Hello world app](hello-world-app.png) 54 | -------------------------------------------------------------------------------- /doc/hello-world-app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/nimx/d93a6795a7e473627e52763deee0cb2df2d1a7fc/doc/hello-world-app.png -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | ==== 2 | Home 3 | ==== 4 | 5 | Welcome to nimx 6 | -------------- 7 | 8 | **nimx** is a UI library written in `Nim`_. Main features: 9 | 10 | **Cross-platform** 11 | nimx can run on Windows, Linux, MacOS X, iOS, Android, and 12 | even `JavaScript`_ in a web-browser! 13 | **Pure** 14 | nimx does not require any non-Nim dependencies except SDL2_ when compiling 15 | to native target. Compiling to JavaScript requires no non-Nim dependencies. 16 | This means that in order to use nimx all you need is SDL2_ and 17 | ``requires nimx`` line in your `.nimble` file. 18 | **Hardware accelerated** 19 | nimx utilizes **OpenGL** renderer under the hood. This means you can embed 20 | nimx into existing **OpenGL** application or use it as a platform for your 21 | application with low-level **OpenGL** rendering. nimx exposes a zero-cost 22 | abstraction over **OpenGL** context to abstract away the difference between 23 | native **OpenGL** and **WebGL**. Despite being efficient for graphics 24 | intensive tasks nimx remains battery-friendly for mobile devices. The screen 25 | is not refreshed constantly when there is nothing to redraw. 26 | **Sleek** 27 | nimx provides instruments for hardware-accelerated resolution-independent 28 | vector graphics rendering which are used by nimx itself to 29 | render controls. Also nimx utilizes signed distance field TTF font rendering, 30 | resulting in better quality and resolution tolerance than conventional 31 | alpha-bitmap font rendering. 32 | 33 | .. _SDL2: https://www.libsdl.org 34 | -------------------------------------------------------------------------------- /doc/sample-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/nimx/d93a6795a7e473627e52763deee0cb2df2d1a7fc/doc/sample-screenshot.png -------------------------------------------------------------------------------- /doc/what-is-nimx.md: -------------------------------------------------------------------------------- 1 | What is nimx 2 | ============ 3 | 4 | 5 | **nimx** is a UI library written in [Nim](http://nim-lang.org). It provides all the needed layers to create a UI application from scratch. Nimx notable features: 6 | 7 | - Cross-platform. Nimx can run on Windows, Linux, MacOS X, iOS, Android, [JavaScript](main.html), Asm.js, WebAssembly, and more. 8 | - Hardware accelerated. Nimx uses OpenGL for the graphics. When no animation is running nimx will redraw the windows only when needed to be power efficient. It can also be switched to high FPS mode so it could be used as a porting layer for games. 9 | - Nimx drawing algorithms are designed to be resolution independent. Shapes are drawn with distance functions, fonts utilize signed distance fields and provide subpixel antialiasing. 10 | - nimx utilizes [kiwi](https://github.com/yglukhov/kiwi) constraint solving algorithm for its layout system allowing for sophisticated layouts defined with an easy to use DSL. 11 | - nimx provides extensible abstractions for cross-platform asset management so that your code looks and works the same regardless you're compiling for a desktop OS, Android or JavaScript. 12 | - nimx provides some essential set of controls and views and makes it easy to implement new ones in separate packages. 13 | -------------------------------------------------------------------------------- /editor/nakefile.nim: -------------------------------------------------------------------------------- 1 | import nimx/naketools 2 | 3 | beforeBuild = proc(b: Builder) = 4 | b.disableClosureCompiler = true 5 | b.mainFile = "nimxedit" 6 | b.appName = "nimxedit" 7 | b.originalResourcePath = "res" 8 | 9 | preprocessResources = proc(b: Builder) = 10 | for f in walkDirRec("res"): 11 | let sf = f.splitFile() 12 | if sf.ext == ".nimx": 13 | b.copyResourceAsIs(f.replace("res/", "")) 14 | -------------------------------------------------------------------------------- /editor/nimxedit.nim: -------------------------------------------------------------------------------- 1 | import tables 2 | 3 | import nimx/matrixes 4 | import nimx/system_logger 5 | import nimx/animation 6 | import nimx/image 7 | import nimx/window 8 | import nimx/autotest 9 | import nimx/button, nimx/text_field 10 | import nimx/all_views 11 | import nimx/editor/edit_view 12 | 13 | const isMobile = defined(ios) or defined(android) 14 | 15 | proc runAutoTestsIfNeeded() = 16 | uiTest generalUITest: 17 | discard 18 | quitApplication() 19 | 20 | registerTest(generalUITest) 21 | when defined(runAutoTests): 22 | startRegisteredTests() 23 | 24 | proc startApplication() = 25 | when isMobile: 26 | var mainWindow = newFullscreenWindow() 27 | else: 28 | var mainWindow = newWindow(newRect(40, 40, 1200, 600)) 29 | mainWindow.title = "nimx" 30 | startNimxEditor(mainWindow) 31 | runAutoTestsIfNeeded() 32 | 33 | runApplication: 34 | startApplication() 35 | -------------------------------------------------------------------------------- /editor/res/assets/back.nimx: -------------------------------------------------------------------------------- 1 | { 2 | "_c": "View", 3 | "name": "", 4 | "frame": { 5 | "origin": { 6 | "x": 0, 7 | "y": 0 8 | }, 9 | "size": { 10 | "width": 1200, 11 | "height": 600 12 | } 13 | }, 14 | "bounds": { 15 | "origin": { 16 | "x": 0, 17 | "y": 0 18 | }, 19 | "size": { 20 | "width": 1200, 21 | "height": 600 22 | } 23 | }, 24 | "subviews": [ 25 | { 26 | "_c": "View", 27 | "name": "background", 28 | "frame": { 29 | "origin": { 30 | "x": 0, 31 | "y": 74 32 | }, 33 | "size": { 34 | "width": 1211, 35 | "height": 500 36 | } 37 | }, 38 | "bounds": { 39 | "origin": { 40 | "x": 0, 41 | "y": 0 42 | }, 43 | "size": { 44 | "width": 1211, 45 | "height": 500 46 | } 47 | }, 48 | "subviews": [], 49 | "arMask": [ 50 | 3, 51 | 4 52 | ], 53 | "color": { 54 | "r": 0.8700000047683716, 55 | "g": 0.8700000047683716, 56 | "b": 0.699999988079071, 57 | "a": 1 58 | } 59 | }, 60 | { 61 | "_c": "View", 62 | "name": "status_bar", 63 | "frame": { 64 | "origin": { 65 | "x": -2, 66 | "y": 573 67 | }, 68 | "size": { 69 | "width": 1209, 70 | "height": 28 71 | } 72 | }, 73 | "bounds": { 74 | "origin": { 75 | "x": 0, 76 | "y": 0 77 | }, 78 | "size": { 79 | "width": 1209, 80 | "height": 28 81 | } 82 | }, 83 | "subviews": [], 84 | "arMask": [ 85 | 3, 86 | 4 87 | ], 88 | "color": { 89 | "r": 0, 90 | "g": 0, 91 | "b": 0, 92 | "a": 0 93 | } 94 | } 95 | ], 96 | "arMask": [ 97 | 4, 98 | 5 99 | ], 100 | "color": { 101 | "r": 0, 102 | "g": 0, 103 | "b": 0, 104 | "a": 0 105 | } 106 | } -------------------------------------------------------------------------------- /editor/res/assets/top_panel.nimx: -------------------------------------------------------------------------------- 1 | {"_c":"View","name":"","frame":{"origin":{"x":0.0,"y":0.0},"size":{"width":1200.0,"height":600.0}},"bounds":{"origin":{"x":0.0,"y":0.0},"size":{"width":1200.0,"height":600.0}},"subviews":[{"_c":"View","name":"parent","frame":{"origin":{"x":-11.0,"y":0.0},"size":{"width":1203.900024414063,"height":74.0}},"bounds":{"origin":{"x":0.0,"y":0.0},"size":{"width":1203.900024414063,"height":74.0}},"subviews":[{"_c":"Button","name":"new_view_btn","frame":{"origin":{"x":10.0,"y":9.0},"size":{"width":122.0,"height":29.0}},"bounds":{"origin":{"x":0.0,"y":0.0},"size":{"width":122.0,"height":29.0}},"subviews":[],"arMask":[1,3],"color":{"r":1.0,"g":1.0,"b":1.0,"a":1.0},"title":"New view","style":0,"behavior":0,"hasBezel":true,"enabled":true,"value":0,"state":1,"imageMarginBottom":2.0,"imageMarginTop":2.0,"imageMarginRight":2.0,"imageMarginLeft":2.0,"imagePath":""},{"_c":"Button","name":"open_btn","frame":{"origin":{"x":142.0,"y":9.0},"size":{"width":108.0,"height":28.0}},"bounds":{"origin":{"x":0.0,"y":0.0},"size":{"width":108.0,"height":28.0}},"subviews":[],"arMask":[1,3],"color":{"r":1.0,"g":1.0,"b":1.0,"a":1.0},"title":"Open","style":0,"behavior":0,"hasBezel":true,"enabled":true,"value":0,"state":1,"imageMarginBottom":2.0,"imageMarginTop":2.0,"imageMarginRight":2.0,"imageMarginLeft":2.0,"imagePath":""},{"_c":"Button","name":"simulate","frame":{"origin":{"x":363.0,"y":9.0},"size":{"width":97.0,"height":31.0}},"bounds":{"origin":{"x":0.0,"y":0.0},"size":{"width":97.0,"height":31.0}},"subviews":[],"arMask":[1,3],"color":{"r":1.0,"g":1.0,"b":1.0,"a":1.0},"title":"Simulate","style":0,"behavior":0,"hasBezel":true,"enabled":true,"value":0,"state":1,"imageMarginBottom":2.0,"imageMarginTop":2.0,"imageMarginRight":2.0,"imageMarginLeft":2.0,"imagePath":""},{"_c":"Button","name":"save_btn","frame":{"origin":{"x":258.0,"y":9.0},"size":{"width":98.0,"height":30.0}},"bounds":{"origin":{"x":0.0,"y":0.0},"size":{"width":98.0,"height":30.0}},"subviews":[],"arMask":[1,3],"color":{"r":1.0,"g":1.0,"b":1.0,"a":1.0},"title":"Save","style":0,"behavior":0,"hasBezel":true,"enabled":true,"value":0,"state":1,"imageMarginBottom":2.0,"imageMarginTop":2.0,"imageMarginRight":2.0,"imageMarginLeft":2.0,"imagePath":""},{"_c":"Button","name":"grid_button","frame":{"origin":{"x":11.0,"y":45.0},"size":{"width":120.0,"height":23.0}},"bounds":{"origin":{"x":0.0,"y":0.0},"size":{"width":120.0,"height":23.0}},"subviews":[],"arMask":[1,3],"color":{"r":1.0,"g":1.0,"b":1.0,"a":1.0},"title":"Grid","style":1,"behavior":1,"hasBezel":true,"enabled":true,"value":0,"state":1,"imageMarginBottom":2.0,"imageMarginTop":2.0,"imageMarginRight":2.0,"imageMarginLeft":2.0,"imagePath":""},{"_c":"View","name":"gridSize","frame":{"origin":{"x":158.0,"y":39.0},"size":{"width":217.0,"height":32.0}},"bounds":{"origin":{"x":0.0,"y":0.0},"size":{"width":217.0,"height":32.0}},"subviews":[],"arMask":[1,3],"color":{"r":0.2838139235973358,"g":0.5471960306167603,"b":0.5775862336158752,"a":1.0}}],"arMask":[1,3,4],"color":{"r":0.1961206793785095,"g":0.5002238750457764,"b":0.6499999761581421,"a":1.0}}],"arMask":[1,3,4,5],"color":{"r":0.0,"g":0.0,"b":0.0,"a":0.0}} -------------------------------------------------------------------------------- /nakefile.nim: -------------------------------------------------------------------------------- 1 | import nimx/naketools 2 | import osproc 3 | 4 | beforeBuild = proc(b: Builder) = 5 | b.disableClosureCompiler = true 6 | b.mainFile = "test/main" 7 | b.originalResourcePath = "test/res" 8 | b.screenOrientation = "sensorLandscape" 9 | b.additionalNimFlags = @["--gc:orc"] 10 | #b.targetArchitectures = @["armeabi-v7a", "arm64-v8a", "x86_64"] 11 | b.targetArchitectures = @["x86"] 12 | b.androidApi = 21 13 | 14 | task "samples", "Build and run samples": 15 | newBuilder().build() 16 | 17 | task "tests", "Build and run autotests": 18 | let b = newBuilder() 19 | 20 | if b.platform == "js": 21 | b.runAfterBuild = false 22 | 23 | b.additionalNimFlags.add "-d:runAutoTests" 24 | b.build() 25 | 26 | if b.platform == "js": 27 | b.runAutotestsInFirefox() 28 | 29 | task "docs", "Build documentation": 30 | createDir "./build/doc" 31 | withDir "./build/doc": 32 | for t, f in walkDir "../../nimx": 33 | if f.endsWith(".nim"): 34 | shell "nim doc2 -d:js " & f & " &>/dev/null" 35 | 36 | for t, f in walkDir "../../doc": 37 | if f.endsWith(".rst"): 38 | direShell "nim rst2html " & f & " &>/dev/null" 39 | 40 | copyDir "../js", "./livedemo" 41 | -------------------------------------------------------------------------------- /nimx.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.2" 4 | author = "Yuriy Glukhov" 5 | description = "GUI framework" 6 | license = "MIT" 7 | 8 | # Directory configuration 9 | installDirs = @["nimx", "assets"] 10 | 11 | # Dependencies 12 | 13 | requires "sdl2" 14 | requires "opengl" 15 | requires "winim" 16 | requires "nimsl >= 0.3" 17 | requires "jnim" # For android target 18 | requires "nake" 19 | requires "closure_compiler >= 0.3.1" 20 | requires "plists" 21 | requires "variant >= 0.2" 22 | requires "kiwi" 23 | requires "https://github.com/yglukhov/ttf >= 0.2.9 & < 0.3" 24 | requires "https://github.com/yglukhov/async_http_request" 25 | requires "jsbind" 26 | requires "rect_packer" 27 | requires "https://github.com/yglukhov/android" 28 | requires "darwin" 29 | requires "os_files" 30 | requires "https://github.com/tormund/nester" 31 | requires "nimwebp" 32 | requires "https://github.com/yglukhov/clipboard" 33 | -------------------------------------------------------------------------------- /nimx/all_views.nim: -------------------------------------------------------------------------------- 1 | import button, text_field, image_view, slider, segmented_control, collection_view 2 | -------------------------------------------------------------------------------- /nimx/animation_runner.nim: -------------------------------------------------------------------------------- 1 | import animation 2 | import times 3 | import logging 4 | 5 | type AnimationRunner* = ref object 6 | animations*: seq[Animation] 7 | onAnimationAdded*: proc() {.gcsafe.} 8 | onAnimationRemoved*: proc() {.gcsafe.} 9 | paused: bool 10 | 11 | proc newAnimationRunner*(): AnimationRunner = AnimationRunner() 12 | 13 | proc pushAnimation*(ar: AnimationRunner, a: Animation) = 14 | if a.isNil: 15 | assert( not a.isNil(), "[AnimationRunner] Animation is nil") 16 | warn "[AnimationRunner] Animation is nil! " 17 | return 18 | 19 | a.prepare(epochTime()) 20 | 21 | if ar.paused: 22 | a.pause() 23 | 24 | if a notin ar.animations: 25 | ar.animations.add(a) 26 | if not ar.onAnimationAdded.isNil(): 27 | ar.onAnimationAdded() 28 | 29 | proc pauseAnimations*(ar: AnimationRunner, isHard: bool = false)= 30 | if isHard: 31 | ar.paused = true 32 | 33 | var index = 0 34 | let animLen = ar.animations.len 35 | 36 | while index < animLen: 37 | var anim = ar.animations[index] 38 | anim.pause() 39 | inc index 40 | 41 | proc resumeAnimations*(ar: AnimationRunner) = 42 | var index = 0 43 | let animLen = ar.animations.len 44 | 45 | while index < animLen: 46 | var anim = ar.animations[index] 47 | anim.resume() 48 | inc index 49 | 50 | ar.paused = false 51 | 52 | proc removeAnimation*(ar: AnimationRunner, a: Animation) = 53 | for idx, anim in ar.animations: 54 | if anim == a: 55 | ar.animations.delete(idx) 56 | if not ar.onAnimationRemoved.isNil(): 57 | ar.onAnimationRemoved() 58 | break 59 | 60 | proc update*(ar: AnimationRunner) = 61 | 62 | var index = 0 63 | let animLen = ar.animations.len 64 | 65 | while index < min(animLen, ar.animations.len): 66 | var anim = ar.animations[index] 67 | if not anim.finished: 68 | anim.tick(epochTime()) 69 | inc index 70 | 71 | index = 0 72 | while index < ar.animations.len: 73 | var anim = ar.animations[index] 74 | if anim.finished: 75 | ar.animations.delete(index) 76 | if not ar.onAnimationRemoved.isNil(): 77 | ar.onAnimationRemoved() 78 | else: 79 | inc index 80 | 81 | proc onDelete*(ar: AnimationRunner) = 82 | ar.onAnimationRemoved = nil 83 | ar.onAnimationAdded = nil 84 | -------------------------------------------------------------------------------- /nimx/app.nim: -------------------------------------------------------------------------------- 1 | import sequtils 2 | 3 | import abstract_window 4 | import event 5 | import window_event_handling 6 | import logging 7 | 8 | type EventFilterControl* = enum 9 | efcContinue 10 | efcBreak 11 | 12 | type EventFilter* = proc(evt: var Event, control: var EventFilterControl): bool {.gcsafe.} 13 | 14 | type Application* = ref object of RootObj 15 | windows : seq[Window] 16 | eventFilters: seq[EventFilter] 17 | inputState: set[VirtualKey] 18 | modifiers: ModifiersSet 19 | 20 | proc pushEventFilter*(a: Application, f: EventFilter) = a.eventFilters.add(f) 21 | 22 | proc newApplication(): Application = 23 | result.new() 24 | result.windows = @[] 25 | result.eventFilters = @[] 26 | result.inputState = {} 27 | 28 | var mainApp {.threadvar.}: Application 29 | 30 | proc mainApplication*(): Application = 31 | if mainApp.isNil: 32 | mainApp = newApplication() 33 | result = mainApp 34 | 35 | proc addWindow*(a: Application, w: Window) = 36 | a.windows.add(w) 37 | 38 | proc removeWindow*(a: Application, w: Window) = 39 | let i = a.windows.find(w) 40 | if i >= 0: a.windows.delete(i) 41 | 42 | proc handleEvent*(a: Application, e: var Event): bool = 43 | if numberOfActiveTouches() == 0 and e.kind == etMouse and e.buttonState == bsUp: 44 | # There may be cases when mouse up is not paired with mouse down. 45 | # This behavior may be observed in Web and native platforms, when clicking on canvas in menu-modal 46 | # mode. We just ignore such events. 47 | warn "Mouse up event ignored" 48 | return false 49 | 50 | if e.kind == etMouse and e.buttonState == bsDown and e.keyCode in a.inputState: 51 | # There may be cases when mouse down is not paired with mouse up. 52 | # This behavior may be observed in Web and native platforms 53 | # We just send mouse bsUp fake event 54 | 55 | var fakeEvent = newMouseButtonEvent(e.position, e.keyCode, bsUp, e.timestamp) 56 | fakeEvent.window = e.window 57 | discard a.handleEvent(fakeEvent) 58 | 59 | beginTouchProcessing(e) 60 | 61 | when not defined(linux) or not defined(nimxAvoidSdl): 62 | if e.kind == etMouse or e.kind == etTouch or e.kind == etKeyboard: 63 | let kc = e.keyCode 64 | let isModifier = kc.isModifier 65 | if e.buttonState == bsDown: 66 | if isModifier: 67 | a.modifiers.incl(kc) 68 | a.inputState.incl(kc) 69 | else: 70 | if isModifier: 71 | a.modifiers.excl(kc) 72 | a.inputState.excl(kc) 73 | 74 | e.modifiers = a.modifiers 75 | 76 | var control = efcContinue 77 | var cleanupEventFilters = false 78 | for i in 0 ..< a.eventFilters.len: 79 | result = a.eventFilters[i](e, control) 80 | if control == efcBreak: 81 | a.eventFilters[i] = nil 82 | cleanupEventFilters = true 83 | control = efcContinue 84 | if result: 85 | break 86 | 87 | if cleanupEventFilters: 88 | a.eventFilters.keepItIf(not it.isNil) 89 | 90 | if not result: 91 | if not e.window.isNil: 92 | result = e.window.handleEvent(e) 93 | elif e.kind in { etAppWillEnterBackground, etAppDidEnterBackground }: 94 | for w in a.windows: w.enableAnimation(false) 95 | elif e.kind in { etAppWillEnterForeground, etAppDidEnterForeground }: 96 | for w in a.windows: w.enableAnimation(true) 97 | 98 | endTouchProcessing(e) 99 | 100 | proc drawWindows*(a: Application) = 101 | for w in a.windows: 102 | if w.needsLayout: 103 | w.updateWindowLayout() 104 | 105 | if w.needsDisplay: 106 | w.drawWindow() 107 | 108 | proc runAnimations*(a: Application) = 109 | for w in a.windows: w.runAnimations() 110 | 111 | proc keyWindow*(a: Application): Window = 112 | if a.windows.len > 0: 113 | result = a.windows[^1] 114 | -------------------------------------------------------------------------------- /nimx/assets/abstract_asset_bundle.nim: -------------------------------------------------------------------------------- 1 | type 2 | AssetBundle* = ref object of RootObj 3 | 4 | proc abstractMethod() = raise newException(Exception, "Abstract method called") 5 | 6 | method allAssets*(ab: AssetBundle): seq[string] {.base.} = abstractMethod() 7 | method urlForPath*(ab: AssetBundle, path: string): string {.base, gcsafe.} = abstractMethod() 8 | 9 | proc allAssetsWithBasePath*(ab: AssetBundle, path: string): seq[string] = 10 | result = ab.allAssets() 11 | for i in 0 ..< result.len: 12 | result[i] = path & '/' & result[i] 13 | -------------------------------------------------------------------------------- /nimx/assets/android_asset_bundle.nim: -------------------------------------------------------------------------------- 1 | import abstract_asset_bundle 2 | import android_asset_url_handler # Required to register the android_asset handler 3 | 4 | type AndroidAssetBundle* = ref object of AssetBundle 5 | 6 | # method forEachAsset*(ab: AssetBundle, action: proc(path: string): bool) = 7 | # raise newException() 8 | 9 | proc newAndroidAssetBundle*(): AndroidAssetBundle = 10 | result.new() 11 | 12 | method urlForPath*(ab: AndroidAssetBundle, path: string): string = 13 | return "android_asset://" & path 14 | -------------------------------------------------------------------------------- /nimx/assets/android_asset_url_handler.nim: -------------------------------------------------------------------------------- 1 | import url_stream 2 | import jnim 3 | import sdl2 4 | import android/ndk/aasset_manager 5 | import android/app/activity 6 | import android/content/res/asset_manager 7 | import android/content/context 8 | 9 | # set jnim jniEnv from sdl 10 | theEnv = cast[JNIEnvPtr](androidGetJNIEnv()) 11 | 12 | proc getAssetManager(): AAssetManager = 13 | result = currentActivity().getApplication().getAssets().getNative() 14 | 15 | let gAssetManager = getAssetManager() 16 | 17 | registerUrlHandler("android_asset") do(url: string, handler: Handler) {.gcsafe.}: 18 | const prefixLen = "android_asset://".len 19 | let p = url.substr(prefixLen) 20 | let s = gAssetManager.streamForReading(p) 21 | var err: string 22 | if s.isNil: 23 | err = "Could not load android asset: " & url 24 | handler(s, err) 25 | -------------------------------------------------------------------------------- /nimx/assets/asset_cache.nim: -------------------------------------------------------------------------------- 1 | import tables 2 | import variant 3 | 4 | type AssetCache* = TableRef[string, Variant] 5 | 6 | template newAssetCache*(): AssetCache = newTable[string, Variant]() 7 | 8 | template registerAsset*(ac: AssetCache, path: string, asset: typed) = 9 | ac[path] = newVariant(asset) 10 | -------------------------------------------------------------------------------- /nimx/assets/asset_loader.nim: -------------------------------------------------------------------------------- 1 | import asset_loading, asset_cache 2 | 3 | const jsEnv = defined(js) or defined(emscripten) 4 | const debugResCache = false 5 | 6 | type AssetLoader* = ref object 7 | when jsEnv: 8 | remainingItemsToLoad: seq[string] 9 | itemsLoading: int 10 | totalSize : int 11 | loadedSize: int 12 | itemsToLoad: int 13 | itemsLoaded: int 14 | onComplete*: proc() {.gcsafe.} 15 | onProgress*: proc(p: float) {.gcsafe.} 16 | assetCache*: AssetCache # Cache to put the loaded resources to. If nil, default cache is used. 17 | when debugResCache: 18 | assetsToLoad: seq[string] 19 | 20 | proc newAssetLoader*(): AssetLoader {.inline.} = 21 | result.new() 22 | 23 | when jsEnv: 24 | proc loadNextAssets(ld: AssetLoader) {.gcsafe.} 25 | 26 | proc onAssetLoaded(ld: AssetLoader, path: string) = 27 | when jsEnv: dec ld.itemsLoading 28 | inc ld.itemsLoaded 29 | when debugResCache: 30 | ld.assetsToLoad.keepIf(proc(a: string):bool = a != path) 31 | echo "REMAINING ITEMS: ", ld.assetsToLoad 32 | if ld.itemsToLoad == ld.itemsLoaded: 33 | ld.onComplete() 34 | if not ld.onProgress.isNil: 35 | ld.onProgress( ld.itemsLoaded.float / ld.itemsToLoad.float) 36 | 37 | when jsEnv: ld.loadNextAssets() 38 | 39 | proc startLoadingAsset(ld: AssetLoader, path: string) = 40 | let url = "res://" & path 41 | loadAsset(url, path, ld.assetCache) do(): 42 | ld.onAssetLoaded(path) 43 | 44 | when jsEnv: 45 | proc loadNextAssets(ld: AssetLoader) = 46 | const parallelLoaders = 10 47 | while ld.itemsLoading < parallelLoaders and ld.remainingItemsToLoad.len > 0: 48 | let next = ld.remainingItemsToLoad.pop() 49 | inc ld.itemsLoading 50 | ld.startLoadingAsset(next) 51 | 52 | proc loadAssets*(ld: AssetLoader, resourceNames: openarray[string]) = 53 | ld.itemsToLoad += resourceNames.len 54 | if ld.assetCache.isNil: 55 | ld.assetCache = newAssetCache() 56 | when debugResCache: 57 | ld.assetsToLoad = @resourceNames 58 | when jsEnv: 59 | ld.remainingItemsToLoad = @resourceNames 60 | ld.loadNextAssets() 61 | else: 62 | for i in resourceNames: 63 | ld.startLoadingAsset(i) 64 | -------------------------------------------------------------------------------- /nimx/assets/json_loading.nim: -------------------------------------------------------------------------------- 1 | import json, logging 2 | import url_stream 3 | 4 | when defined(js) or defined(emscripten): 5 | import jsbind, web_url_handler 6 | 7 | proc loadJsonFromURL*(url: string, handler: proc(j: JsonNode) {.gcsafe.}) = 8 | when defined(js): 9 | let reqListener = proc(str: JSObj) = 10 | handler(parseJson($(cast[cstring](str)))) 11 | loadJSURL(url, "text", nil, nil, reqListener) 12 | else: 13 | openStreamForUrl(url) do(s: Stream, err: string): 14 | if err.len == 0: 15 | handler(parseJson(s, url)) 16 | s.close() 17 | else: 18 | error "Error loading json from url (", url, "): ", err 19 | handler(nil) 20 | -------------------------------------------------------------------------------- /nimx/assets/native_asset_bundle.nim: -------------------------------------------------------------------------------- 1 | import os 2 | import abstract_asset_bundle 3 | 4 | type NativeAssetBundle* = ref object of AssetBundle 5 | mBaseUrl: string 6 | 7 | proc newNativeAssetBundle*(): NativeAssetBundle = 8 | result.new() 9 | when defined(ios): 10 | result.mBaseUrl = "file://" & getAppDir() 11 | elif defined(macosx): 12 | result.mBaseUrl = "file://" & getAppDir() & "/../Resources" 13 | else: 14 | result.mBaseUrl = "file://" & getAppDir() & "/res" 15 | 16 | method urlForPath*(ab: NativeAssetBundle, path: string): string = 17 | return ab.mBaseUrl & "/" & path 18 | -------------------------------------------------------------------------------- /nimx/assets/url_stream.nim: -------------------------------------------------------------------------------- 1 | import streams, strutils, tables 2 | 3 | export streams 4 | 5 | type Error = string 6 | type Handler* = proc(s: Stream, error: Error) {.gcsafe.} 7 | type UrlHandler = proc(url: string, handler: Handler) {.gcsafe, nimcall.} 8 | 9 | var urlHandlers: Table[string, UrlHandler] 10 | 11 | proc urlScheme(s: string): string = 12 | let i = s.find(':') - 1 13 | if i > 0: 14 | result = s.substr(0, i) 15 | 16 | proc openStreamForUrl*(url: string, handler: Handler) {.gcsafe.} = 17 | assert(not handler.isNil) 18 | let scheme = url.urlScheme 19 | if scheme.len == 0: 20 | raise newException(Exception, "Invalid url: \"" & url & "\"") 21 | var uh: UrlHandler 22 | {.gcsafe.}: 23 | uh = urlHandlers.getOrDefault(scheme) 24 | if uh.isNil: 25 | raise newException(Exception, "No url handler for scheme " & scheme) 26 | uh(url, handler) 27 | 28 | proc registerUrlHandler*(scheme: string, handler: UrlHandler) = 29 | assert(scheme notin urlHandlers) 30 | urlHandlers[scheme] = handler 31 | 32 | proc getPathFromFileUrl(url: string): string = 33 | const prefixLen = len("file://") 34 | result = substr(url, prefixLen) 35 | 36 | when not defined(js) and not defined(emscripten): 37 | registerUrlHandler("file") do(url: string, handler: Handler) {.gcsafe.}: 38 | let p = getPathFromFileUrl(url) 39 | let s = newFileStream(p, fmRead) 40 | if s.isNil: 41 | handler(nil, "Could not open file: " & p) 42 | else: 43 | handler(s, "") 44 | -------------------------------------------------------------------------------- /nimx/assets/web_asset_bundle.nim: -------------------------------------------------------------------------------- 1 | import os, strutils 2 | import abstract_asset_bundle, url_stream 3 | 4 | import nimx/pathutils 5 | 6 | var resourceUrlMapper*: proc(p: string): string # Deprecated 7 | 8 | when defined(js) or defined(emscripten): 9 | proc urlForResourcePath*(path: string): string {.deprecated.} = 10 | if resourceUrlMapper.isNil: 11 | path 12 | else: 13 | resourceUrlMapper(path) 14 | 15 | type WebAssetBundle* = ref object of AssetBundle 16 | mHref: string 17 | mBaseUrl: string 18 | 19 | proc newWebAssetBundle*(): WebAssetBundle = 20 | result.new() 21 | result.mHref = urlParentDir(getCurrentHref()) 22 | result.mBaseUrl = result.mHref & "/res" 23 | 24 | method urlForPath*(ab: WebAssetBundle, path: string): string = 25 | {.gcsafe.}: 26 | if resourceUrlMapper.isNil: 27 | result = ab.mBaseUrl & "/" & path 28 | else: 29 | result = resourceUrlMapper("res/" & path) 30 | if not result.startsWith("http"): 31 | result = ab.mHref & "/" & result 32 | -------------------------------------------------------------------------------- /nimx/clip_view.nim: -------------------------------------------------------------------------------- 1 | import nimx / view 2 | import nimx / meta_extensions / [ property_desc, visitors_gen, serializers_gen ] 3 | 4 | type ClipView* = ref object of View 5 | 6 | proc newClipView*(r: Rect): ClipView = 7 | result.new() 8 | result.init(r) 9 | result.autoresizingMask = { afFlexibleWidth, afFlexibleHeight } 10 | 11 | method subviewDidChangeDesiredSize*(v: ClipView, sub: View, desiredSize: Size) = 12 | v.superview.subviewDidChangeDesiredSize(v, desiredSize) 13 | 14 | method clipType*(v: ClipView): ClipType = ctDefaultClip 15 | 16 | proc enclosingClipView*(v: View): ClipView = v.enclosingViewOfType(ClipView) 17 | 18 | registerClass(ClipView) 19 | genVisitorCodeForView(ClipView) 20 | genSerializeCodeForView(ClipView) -------------------------------------------------------------------------------- /nimx/color.nim: -------------------------------------------------------------------------------- 1 | import types 2 | 3 | proc titleBarColor*(): Color = newColor(0.2, 0.2, 0.2) 4 | proc contentViewColor*(): Color = newColor(0.5, 0.5, 0.5) 5 | proc titleTextColor*(): Color = newColor(0.9, 0.9, 0.9) 6 | -------------------------------------------------------------------------------- /nimx/common/expand_button.nim: -------------------------------------------------------------------------------- 1 | import nimx / [ view, context, button ] 2 | import math 3 | 4 | type ExpandButton* = ref object of Button 5 | expanded*: bool 6 | onExpandAction*: proc(state: bool) {.gcsafe.} 7 | 8 | method init*(b: ExpandButton, r: Rect) = 9 | procCall b.Button.init(r) 10 | 11 | b.enable() 12 | b.onAction() do(): 13 | b.expanded = not b.expanded 14 | if not b.onExpandAction.isNil: 15 | b.onExpandAction(b.expanded) 16 | 17 | proc newExpandButton*(r: Rect): ExpandButton = 18 | result.new() 19 | result.init(r) 20 | 21 | proc newExpandButton*(v: View, r: Rect): ExpandButton = 22 | result = newExpandButton(r) 23 | v.addSubview(result) 24 | 25 | method draw*(b: ExpandButton, r: Rect) = 26 | let c = currentContext() 27 | c.fillColor = newColor(0.9, 0.9, 0.9) 28 | c.drawTriangle(r, if b.expanded: Coord(PI / 2.0) else: Coord(0)) 29 | -------------------------------------------------------------------------------- /nimx/control.nim: -------------------------------------------------------------------------------- 1 | 2 | import view, view_event_handling 3 | export view 4 | 5 | type Control* = ref object of View 6 | actionHandler: proc(e: Event) {.gcsafe.} 7 | clickable*: bool 8 | 9 | method sendAction*(c: Control, e: Event) {.base, gcsafe.} = 10 | if not c.actionHandler.isNil: 11 | c.actionHandler(e) 12 | 13 | proc sendAction*(c: Control) = 14 | # Send action with unknown event 15 | c.sendAction(newUnknownEvent()) 16 | 17 | proc onAction*(c: Control, handler: proc(e: Event) {.gcsafe.}) = 18 | c.actionHandler = handler 19 | 20 | proc onAction*(c: Control, handler: proc() {.gcsafe.}) = 21 | if handler.isNil: 22 | c.actionHandler = nil 23 | else: 24 | c.onAction do (e: Event): 25 | handler() 26 | 27 | method onTouchEv*(b: Control, e: var Event): bool = 28 | discard procCall b.View.onTouchEv(e) 29 | if b.clickable: 30 | case e.buttonState 31 | of bsUp: 32 | if e.localPosition.inRect(b.bounds): 33 | b.sendAction(e) 34 | else: discard 35 | result = true 36 | -------------------------------------------------------------------------------- /nimx/drag_and_drop.nim: -------------------------------------------------------------------------------- 1 | import view 2 | import types 3 | import class_registry 4 | import variant 5 | 6 | import nimx/image 7 | import nimx/pasteboard/pasteboard_item 8 | 9 | type DragSystem* = ref object 10 | rect*: Rect 11 | itemPosition*: Point 12 | pItem*: PasteboardItem 13 | prevTarget*: View 14 | image*: Image 15 | 16 | var gDragSystem {.threadvar.}: DragSystem 17 | 18 | proc currentDragSystem*(): DragSystem = 19 | if gDragSystem.isNil: 20 | gDragSystem = new(DragSystem) 21 | gDragSystem.rect = newRect(0, 0, 30, 30) 22 | 23 | result = gDragSystem 24 | 25 | 26 | proc startDrag*(item: PasteboardItem, image: Image = nil) = 27 | currentDragSystem().pItem = item 28 | currentDragSystem().image = image 29 | currentDragSystem().prevTarget = nil 30 | 31 | proc stopDrag*() = 32 | currentDragSystem().pItem = nil 33 | currentDragSystem().prevTarget = nil 34 | 35 | proc newDragDestinationDelegate*(): DragDestinationDelegate = 36 | result.new() 37 | 38 | method onDrag*(dd: DragDestinationDelegate, target: View, i: PasteboardItem) {.base, gcsafe.} = discard 39 | method onDrop*(dd: DragDestinationDelegate, target: View, i: PasteboardItem) {.base, gcsafe.} = discard 40 | method onDragEnter*(dd: DragDestinationDelegate, target: View, i: PasteboardItem) {.base, gcsafe.} = discard 41 | method onDragExit*(dd: DragDestinationDelegate, target: View, i: PasteboardItem) {.base, gcsafe.} = discard 42 | 43 | proc findSubviewAtPoint*(v: View, p: Point, res: var View) = 44 | for i in countdown(v.subviews.len - 1, 0): 45 | let s = v.subviews[i] 46 | var pp = s.convertPointFromParent(p) 47 | if pp.inRect(s.bounds): 48 | s.findSubviewAtPoint(pp, res) 49 | if not res.isNil: 50 | break 51 | 52 | 53 | proc findSubviewAtPointAux*(v: View, p: Point, target: var View): View = 54 | for i in countdown(v.subviews.len - 1, 0): 55 | let s = v.subviews[i] 56 | var pp = s.convertPointFromParent(p) 57 | if pp.inRect(s.bounds): 58 | if not v.dragDestination.isNil: 59 | target = v 60 | result = s.findSubviewAtPointAux(pp, target) 61 | if not result.isNil: 62 | break 63 | 64 | if result.isNil: 65 | result = v 66 | if not result.dragDestination.isNil: 67 | target = result 68 | 69 | 70 | proc findSubviewAtPoint*(v: View, p: Point): View = 71 | discard v.findSubviewAtPointAux(p, result) 72 | if result == v: result = nil 73 | 74 | 75 | -------------------------------------------------------------------------------- /nimx/editor/bezier_view.nim: -------------------------------------------------------------------------------- 1 | import nimx / [ types, view, context, event, view_event_handling, gesture_detector, keyboard ] 2 | 3 | 4 | type 5 | BezierView* = ref object of View 6 | pointOne: Point 7 | pointTwo: Point 8 | key: bool 9 | mOnAction: proc() {.gcsafe.} 10 | 11 | proc onAction*(v: BezierView, cb: proc() {.gcsafe.})= 12 | v.mOnAction = cb 13 | 14 | proc p1*(v: BezierView): float = v.pointOne.x / v.bounds.width 15 | 16 | proc `p1=`*(v: BezierView, val: float)= 17 | v.pointOne.x = val * v.bounds.width 18 | 19 | proc p2*(v: BezierView): float = (v.bounds.height - v.pointOne.y) / v.bounds.height 20 | 21 | proc `p2=`*(v: BezierView, val: float)= 22 | v.pointOne.y = v.bounds.height - val * v.bounds.height 23 | 24 | proc p3*(v: BezierView): float = v.pointTwo.x / v.bounds.width 25 | 26 | proc `p3=`*(v: BezierView, val: float)= 27 | v.pointTwo.x = val * v.bounds.width 28 | 29 | proc p4*(v: BezierView): float = (v.bounds.height - v.pointTwo.y) / v.bounds.width 30 | 31 | proc `p4=`*(v: BezierView, val: float)= 32 | v.pointTwo.y = v.bounds.height - val * v.bounds.height 33 | 34 | method onTouchEv*(v: BezierView, e: var Event): bool = 35 | if e.buttonState == bsDown: 36 | v.key = e.keyCode == VirtualKey.MouseButtonPrimary 37 | 38 | else: 39 | var actionHappend: bool 40 | if v.key: 41 | actionHappend = v.pointOne != e.localPosition 42 | v.pointOne = e.localPosition 43 | else: 44 | actionHappend = v.pointTwo != e.localPosition 45 | v.pointTwo = e.localPosition 46 | 47 | if actionHappend and not v.mOnAction.isNil: 48 | v.mOnAction() 49 | 50 | v.window.setNeedsDisplay() 51 | 52 | result = true 53 | 54 | method init*(v: BezierView, r: Rect)= 55 | procCall v.View.init(r) 56 | v.backgroundColor = newColor(0.7, 0.7, 0.7, 1.0) 57 | 58 | method draw*(v: BezierView, r: Rect) = 59 | procCall v.View.draw(r) 60 | 61 | let c = currentContext() 62 | 63 | let botLeft = newPoint(0, v.bounds.height) 64 | let topRight = newPoint(v.bounds.width, 0.0) 65 | 66 | c.strokeWidth = 3 67 | c.strokeColor = newColor(0.0, 0.0, 0.0, 0.5) 68 | c.drawLine(botLeft, topRight) 69 | 70 | c.strokeWidth = 4 71 | c.strokeColor = blackColor() 72 | c.drawBezier(botLeft, v.pointOne, v.pointTwo, topRight) 73 | 74 | c.strokeWidth = 3 75 | c.strokeColor = newColor(0.8, 0.2, 0.2, 0.5) 76 | c.drawLine(botLeft, v.pointOne) 77 | 78 | c.strokeWidth = 3 79 | c.strokeColor = newColor(0.2, 0.8, 0.2, 0.5) 80 | c.drawLine(topRight, v.pointTwo) 81 | 82 | -------------------------------------------------------------------------------- /nimx/editor/editor_types.nim: -------------------------------------------------------------------------------- 1 | import nimx / [ 2 | view, panel_view, toolbar, button, menu, undo_manager, 3 | inspector_panel, gesture_detector, window_event_handling, event, view_event_handling, 4 | serializers, key_commands, pasteboard/pasteboard, property_editors/autoresizing_mask_editor 5 | ] 6 | 7 | import ui_document 8 | import grid_drawing 9 | 10 | type 11 | EventCatchingView* = ref object of View 12 | keyUpDelegate*: proc (event: var Event) {.gcsafe.} 13 | keyDownDelegate*: proc (event: var Event) {.gcsafe.} 14 | mouseScrrollDelegate*: proc (event: var Event) {.gcsafe.} 15 | panningView*: View # View that we're currently moving/resizing with `panOp` 16 | editor*: Editor 17 | panOp*: PanOperation 18 | dragStartTime*: float 19 | origPanRect*: Rect 20 | origPanPoint*: Point 21 | mGridSize*: float 22 | 23 | EditView* = ref object of View 24 | editor*: Editor 25 | 26 | UIDocument* = ref object 27 | view*: View 28 | undoManager*: UndoManager 29 | path*: string 30 | takenViewNames*: seq[string] #used only for propose default names 31 | 32 | Editor* = ref object 33 | eventCatchingView*: EventCatchingView 34 | inspector*: InspectorPanel 35 | mSelectedView*: View # View that we currently draw selection rect around 36 | document*: UIDocument 37 | workspace*: EditorWorkspace 38 | 39 | PanOperation* = enum 40 | poDrag 41 | poDragTL 42 | poDragT 43 | poDragTR 44 | poDragB 45 | poDragBR 46 | poDragBL 47 | poDragL 48 | poDragR 49 | 50 | EditorWorkspace* = ref object of View 51 | gridSize*: Size 52 | -------------------------------------------------------------------------------- /nimx/editor/editor_workspace.nim: -------------------------------------------------------------------------------- 1 | import nimx / [ types, view ] 2 | import grid_drawing 3 | import editor_types 4 | 5 | method draw*(v: EditorWorkspace, r: Rect)= 6 | procCall v.View.draw(r) 7 | 8 | if v.gridSize != zeroSize: 9 | drawGrid(v.bounds, v.gridSize) 10 | 11 | 12 | -------------------------------------------------------------------------------- /nimx/editor/grid_drawing.nim: -------------------------------------------------------------------------------- 1 | {.used.} 2 | import nimx/context 3 | import nimx/types 4 | 5 | proc drawGrid*(bounds: Rect, gridSize: Size, shift = zeroSize) = 6 | let c = currentContext() 7 | c.strokeWidth = 0 8 | c.strokeColor = blackColor() 9 | 10 | c.fillColor = blackColor() 11 | 12 | var r = newRect(0, 0, 1, bounds.height) 13 | if gridSize.width > 0: 14 | let n = int((bounds.width - bounds.x - shift.width) / gridSize.width) 15 | for x in 0 .. n: 16 | r.origin.x = bounds.x + shift.width + Coord(x) * gridSize.width 17 | c.drawRect(r) 18 | if gridSize.height > 0: 19 | r.origin.x = 0 20 | r.size.width = bounds.width 21 | r.size.height = 1 22 | let n = int((bounds.height - bounds.y - shift.height) / gridSize.height) 23 | for y in 0 .. n: 24 | r.origin.y = bounds.y + shift.height + Coord(y) * gridSize.height 25 | c.drawRect(r) 26 | -------------------------------------------------------------------------------- /nimx/event.nim: -------------------------------------------------------------------------------- 1 | import types 2 | import unicode 3 | import abstract_window 4 | 5 | import keyboard 6 | export keyboard 7 | 8 | type EventType* = enum 9 | etUnknown 10 | etMouse 11 | etTouch 12 | etScroll 13 | etKeyboard 14 | etWindowResized 15 | etTextInput 16 | etTextEditing 17 | etAppWillEnterBackground 18 | etAppWillEnterForeground 19 | etAppDidEnterBackground 20 | etAppDidEnterForeground 21 | 22 | type ButtonState* = enum 23 | bsUnknown 24 | bsUp 25 | bsDown 26 | 27 | type Event* = object 28 | timestamp*: uint32 29 | kind*: EventType 30 | pointerId*: int # raw touchId 31 | position*: Point 32 | localPosition*: Point 33 | offset*: Point 34 | keyCode*: VirtualKey 35 | buttonState*: ButtonState 36 | rune*: Rune 37 | repeat*: bool 38 | window*: Window 39 | text*: string 40 | modifiers*: ModifiersSet 41 | 42 | proc newEvent*(kind: EventType, position: Point = zeroPoint, keyCode: VirtualKey = VirtualKey.Unknown, 43 | buttonState: ButtonState = bsUnknown, pointerId : int = 0, timestamp : uint32 = 0): Event = 44 | result.kind = kind 45 | result.position = position 46 | result.localPosition = position 47 | result.keyCode = keyCode 48 | result.buttonState = buttonState 49 | result.pointerId = pointerId 50 | result.timestamp = timestamp 51 | 52 | proc newUnknownEvent*(): Event = newEvent(etUnknown) 53 | 54 | proc newMouseMoveEvent*(position: Point, tstamp : uint32): Event = 55 | newEvent(etMouse, position, VirtualKey.Unknown, bsUnknown, 0, tstamp) 56 | 57 | proc newMouseMoveEvent*(position: Point): Event = 58 | newEvent(etMouse, position, VirtualKey.Unknown, bsUnknown, 0, 0) 59 | 60 | proc newMouseButtonEvent*(position: Point, button: VirtualKey, state: ButtonState, tstamp : uint32): Event = 61 | newEvent(etMouse, position, button, state, 0, tstamp) 62 | 63 | proc newMouseButtonEvent*(position: Point, button: VirtualKey, state: ButtonState): Event = 64 | newEvent(etMouse, position, button, state, 0, 0) 65 | 66 | proc newTouchEvent*(position: Point, state: ButtonState, pointerId : int, tstamp : uint32): Event = 67 | newEvent(etTouch, position, VirtualKey.Unknown, state, pointerId, tstamp) 68 | 69 | proc newMouseDownEvent*(position: Point, button: VirtualKey): Event = 70 | newMouseButtonEvent(position, button, bsDown,0) 71 | 72 | proc newMouseUpEvent*(position: Point, button: VirtualKey): Event = 73 | newMouseButtonEvent(position, button, bsUp,0) 74 | 75 | proc newKeyboardEvent*(keyCode: VirtualKey, buttonState: ButtonState, repeat: bool = false): Event = 76 | result = newEvent(etKeyboard, zeroPoint, keyCode, buttonState) 77 | result.repeat = repeat 78 | 79 | proc isPointingEvent*(e: Event) : bool = 80 | result = e.pointerId == 0 and (e.kind == etMouse or e.kind == etTouch) 81 | proc isButtonDownEvent*(e: Event): bool = e.buttonState == bsDown 82 | proc isButtonUpEvent*(e: Event): bool = e.buttonState == bsUp 83 | 84 | proc isMouseMoveEvent*(e: Event): bool = e.buttonState == bsUnknown and e.kind == etMouse 85 | 86 | var activeTouches = 0 87 | 88 | template numberOfActiveTouches*(): int = activeTouches 89 | 90 | proc incrementActiveTouchesIfNeeded(e: Event) = 91 | if e.buttonState == bsDown: 92 | inc activeTouches 93 | assert(activeTouches > 0) 94 | 95 | proc decrementActiveTouchesIfNeeded(e: Event) = 96 | if e.buttonState == bsUp: 97 | assert(activeTouches > 0) 98 | dec activeTouches 99 | 100 | # Private proc. Should be called from application.handleEvent() 101 | proc beginTouchProcessing*(e: var Event)= 102 | if (e.kind == etTouch or e.kind == etMouse): 103 | e.incrementActiveTouchesIfNeeded() 104 | 105 | # Private proc. Should be called from application.handleEvent() 106 | proc endTouchProcessing*(e: var Event)= 107 | if (e.kind == etTouch or e.kind == etMouse): 108 | e.decrementActiveTouchesIfNeeded() 109 | -------------------------------------------------------------------------------- /nimx/expanding_view.nim: -------------------------------------------------------------------------------- 1 | import nimx/font 2 | import nimx/button 3 | import nimx/view 4 | import nimx/context 5 | import nimx/types 6 | import nimx/color 7 | 8 | import nimx/stack_view 9 | import nimx/common/expand_button 10 | 11 | const titleSize = 20.0 12 | const expandButtonSize = 20.0 13 | 14 | type ExpandingView* = ref object of View 15 | title*: string 16 | contentView*: View 17 | expanded*: bool 18 | hasOffset*: bool 19 | expandBut*: ExpandButton 20 | isDraggable*: bool 21 | isDragged: bool 22 | dragPoint: Point 23 | titleBarColor*: Color 24 | titleTextColor*: Color 25 | 26 | # initRect: Rect 27 | 28 | proc updateFrame(v: ExpandingView) = 29 | var expandRect = v.contentView.frame 30 | if v.hasOffset: 31 | expandRect.size.width = expandRect.width + expandButtonSize 32 | 33 | expandRect.origin = v.frame.origin 34 | if v.expanded: 35 | expandRect.size.height = expandRect.height + titleSize 36 | v.setFrame(expandRect) 37 | else: 38 | expandRect.size.height = titleSize 39 | v.setFrame(newRect(v.frame.x, v.frame.y, expandRect.size.width, titleSize)) 40 | 41 | if not v.superview.isNil: 42 | v.superview.subviewDidChangeDesiredSize(v, v.frame().size) 43 | 44 | if v.expanded: 45 | if v.contentView.superview.isNil: 46 | v.addSubview v.contentView 47 | elif not v.contentView.superview.isNil: 48 | v.contentView.removeFromSuperView() 49 | 50 | proc init*(v: ExpandingView, r: Rect, hasOffset: bool) = 51 | procCall v.View.init(r) 52 | v.backgroundColor = newColor(0.2, 0.2, 0.2, 1.0) 53 | v.title = "Expanded View" 54 | 55 | v.hasOffset = hasOffset 56 | if v.hasOffset: 57 | v.contentView = newStackView(newRect(expandButtonSize, titleSize, r.width - expandButtonSize, r.height - titleSize)) 58 | else: 59 | v.contentView = newStackView(newRect(0, titleSize, r.width, r.height - titleSize)) 60 | v.contentView.name = "contentView" 61 | v.contentView.resizingMask = "wb" 62 | v.addSubview(v.contentView) 63 | 64 | v.expandBut = newExpandButton(v, newRect(0.0, 0.0, expandButtonSize, expandButtonSize)) 65 | v.expandBut.onExpandAction = proc(state: bool) = 66 | v.expanded = state 67 | v.updateFrame() 68 | 69 | v.titleBarColor = titleBarColor() 70 | v.titleTextColor = titleTextColor() 71 | v.updateFrame() 72 | 73 | proc expand*(v: ExpandingView) = 74 | v.expanded = true 75 | v.updateFrame() 76 | v.expandBut.expanded = true 77 | 78 | proc newExpandingView*(r: Rect, hasOffset: bool = false): ExpandingView = 79 | result.new() 80 | result.init(r, hasOffset) 81 | result.name = "expandingView" 82 | 83 | method draw(v: ExpandingView, r: Rect) = 84 | procCall v.View.draw(r) 85 | 86 | # title 87 | let c = currentContext() 88 | var titleRect: Rect 89 | titleRect.size.width = r.width 90 | titleRect.size.height = titleSize 91 | 92 | c.fillColor = v.titleBarColor 93 | c.drawRect(titleRect) 94 | 95 | c.fillColor = v.titleTextColor 96 | c.drawText(systemFontOfSize(14.0), newPoint(25, 1), v.title) 97 | 98 | 99 | v.contentView.hidden = not v.expanded 100 | 101 | proc addContent*(v: ExpandingView, subView: View) = 102 | v.contentView.addSubview(subView) 103 | v.updateFrame() 104 | 105 | # method onTouchEv*(v: ExpandingView, e: var Event) : bool = 106 | # discard procCall v.View.onTouchEv(e) 107 | # result = true 108 | 109 | method subviewDidChangeDesiredSize*(v: ExpandingView, sub: View, desiredSize: Size) = 110 | v.updateFrame() 111 | 112 | method clipType*(v: ExpandingView): ClipType = ctDefaultClip 113 | -------------------------------------------------------------------------------- /nimx/form_view.nim: -------------------------------------------------------------------------------- 1 | 2 | import view 3 | import text_field 4 | import tables 5 | 6 | type FormView* = ref object of View 7 | labelsMap: Table[string, int] 8 | 9 | proc newFormView*(r: Rect, numberOfFields: int, adjustFrameHeight: bool = true): FormView = 10 | result.new() 11 | let fieldHeight = 20.Coord 12 | var fr = r 13 | if adjustFrameHeight: 14 | fr.size.height = fieldHeight * numberOfFields.Coord 15 | 16 | result.init(fr) 17 | result.labelsMap = initTable[string, int]() 18 | for i in 0 .. < numberOfFields: 19 | let label = newLabel(newRect(0, i.Coord * fieldHeight, r.width / 3, fieldHeight)) 20 | result.addSubview(label) 21 | let value = newTextField(newRect(r.width / 3, i.Coord * fieldHeight, r.width / 3 * 2, fieldHeight)) 22 | result.addSubview(value) 23 | 24 | proc labelAtIndex(v: FormView, index: int): TextField = TextField(v.subviews[index * 2]) 25 | proc inputAtIndex(v: FormView, index: int): TextField = TextField(v.subviews[index * 2 + 1]) 26 | proc inputForLabel(v: FormView, label: string): TextField = v.inputAtIndex(v.labelsMap[label]) 27 | 28 | proc labelValue*(v: FormView, index: int): string = v.labelAtIndex(index).text 29 | 30 | proc setLabel*(v: FormView, index: int, label: string) = 31 | let oldLabel = v.labelValue(index) 32 | v.labelsMap.del(oldLabel) 33 | v.labelsMap[label] = index 34 | v.labelAtIndex(index).text = label & ":" 35 | 36 | proc setValue*(v: FormView, index: int, value: string) = 37 | v.inputAtIndex(index).text = value 38 | 39 | proc setValue*(v: FormView, label: string, value: string) = 40 | v.inputForLabel(label).text = value 41 | 42 | proc newFormView*(r: Rect, fieldNames: openarray[string], adjustFrameHeight: bool = true): FormView = 43 | result = newFormView(r, fieldNames.len, adjustFrameHeight) 44 | for i, n in fieldNames: 45 | result.setLabel(i, n) 46 | result.setValue(i, "") 47 | 48 | proc inputValue*(v: FormView, index: int): string = v.inputAtIndex(index).text 49 | proc inputValue*(v: FormView, label: string): string = v.inputForLabel(label).text 50 | 51 | -------------------------------------------------------------------------------- /nimx/gesture_detector_newtouch.nim: -------------------------------------------------------------------------------- 1 | {.deprecated.} 2 | import gesture_detector 3 | export gesture_detector 4 | -------------------------------------------------------------------------------- /nimx/http_request.nim: -------------------------------------------------------------------------------- 1 | import async_http_request 2 | export async_http_request 3 | 4 | when not defined(js) and not defined(emscripten): 5 | import perform_on_main_thread, marshal, streams 6 | 7 | proc storeToSharedBuffer*[T](a: T): pointer = 8 | let s = newStringStream() 9 | store(s, a) 10 | result = allocShared(s.data.len + sizeof(uint64)) 11 | cast[ptr uint64](result)[] = s.data.len.uint64 12 | copyMem(cast[pointer](cast[int](result) + sizeof(uint64)), addr s.data[0], s.data.len) 13 | s.close() 14 | 15 | proc readFromSharedBuffer*[T](p: pointer, res: var T) = 16 | let bufLen = cast[ptr uint64](p)[] 17 | var str = newStringOfCap(bufLen) 18 | str.setLen(bufLen) 19 | copyMem(addr str[0], cast[pointer](cast[int](p) + sizeof(uint64)), bufLen) 20 | let s = newStringStream(str) 21 | load(s, res) 22 | s.close() 23 | 24 | proc sendRequest*(meth, url, body: string, headers: openarray[(string, string)], handler: Handler) = 25 | type SdlHandlerContext = ref object 26 | handler: Handler 27 | data: pointer 28 | 29 | var ctx: SdlHandlerContext 30 | ctx.new() 31 | ctx.handler = handler 32 | GC_ref(ctx) 33 | 34 | proc callHandler(c: pointer) {.cdecl.} = 35 | let ctx = cast[SdlHandlerContext](c) 36 | var r: Response 37 | readFromSharedBuffer(ctx.data, r) 38 | deallocShared(ctx.data) 39 | ctx.handler(r) 40 | GC_unref(ctx) 41 | 42 | proc sdlThreadSafeHandler(r: Response, ctx: pointer) {.nimcall.} = 43 | cast[SdlHandlerContext](ctx).data = storeToSharedBuffer(r) 44 | performOnMainThread(callHandler, ctx) 45 | 46 | when compileOption("threads"): 47 | sendRequestThreaded(meth, url, body, headers, sdlThreadSafeHandler, cast[pointer](ctx)) 48 | else: 49 | doAssert(false, "[Not implemented] Http requests only work with --threads:on") 50 | -------------------------------------------------------------------------------- /nimx/image_view.nim: -------------------------------------------------------------------------------- 1 | import nimx / [ context, image, types, view ] 2 | import nimx / meta_extensions / [ property_desc, visitors_gen, serializers_gen ] 3 | 4 | type 5 | ImageFillRule* {.pure.} = enum 6 | ## Defines how image is drawn inside view 7 | NoFill ## Image is drawn from top-left corner with its size 8 | Stretch ## Image is stretched to view size 9 | Tile ## Image tiles all view 10 | FitWidth ## Image fits view's width 11 | FitHeight ## Image fits view's height 12 | NinePartImage 13 | 14 | ImageView* = ref object of View 15 | ## Image view is a view for drawing static images 16 | ## with certain view filling rules 17 | image: Image 18 | fillRule: ImageFillRule 19 | imageMarginLeft*: Coord 20 | imageMarginRight*: Coord 21 | imageMarginTop*: Coord 22 | imageMarginBottom*: Coord 23 | 24 | proc newImageView*(r: Rect, image: Image = nil, fillRule = ImageFillRule.NoFill): ImageView = 25 | result.new 26 | result.image = image 27 | result.fillRule = fillRule 28 | result.init(r) 29 | 30 | method init*(v: ImageView, r: Rect) = 31 | procCall v.View.init(r) 32 | 33 | proc image*(v: ImageView): Image = v.image 34 | proc `image=`*(v: ImageView, image: Image) = 35 | v.image = image 36 | v.setNeedsDisplay() 37 | 38 | proc fillRule*(v: ImageView): ImageFillRule = v.fillRule 39 | proc `fillRule=`*(v: ImageView, fillRule: ImageFillRule) = 40 | v.fillRule = fillRule 41 | v.setNeedsDisplay() 42 | 43 | method clipType*(v: ImageView): ClipType = ctDefaultClip 44 | 45 | method draw*(v: ImageView, r: Rect) = 46 | procCall v.View.draw(r) 47 | let c = currentContext() 48 | 49 | if v.image.isNil(): 50 | c.drawRect(r) 51 | c.drawLine(newPoint(r.x, r.y), newPoint(r.x + r.width, r.y + r.height)) 52 | c.drawLine(newPoint(r.x, r.y + r.height), newPoint(r.x + r.width, r.y)) 53 | else: 54 | case v.fillRule 55 | of ImageFillRule.NoFill: 56 | c.drawImage(v.image, newRect(r.x, r.y, v.image.size.width, v.image.size.height)) 57 | of ImageFillRule.Stretch: 58 | c.drawImage(v.image, r) 59 | of ImageFillRule.Tile: 60 | let cols = r.width.int div v.image.size.width.int + 1 61 | let rows = r.height.int div v.image.size.height.int + 1 62 | for col in 0 ..< cols: 63 | for row in 0 ..< rows: 64 | let imageRect = newRect(col.Coord * v.image.size.width, row.Coord * v.image.size.height, v.image.size.width, v.image.size.height) 65 | c.drawImage(v.image, imageRect) 66 | of ImageFillRule.FitWidth: 67 | let 68 | stretchRatio = r.width / v.image.size.width 69 | newWidth = r.width 70 | newHeight = v.image.size.height * stretchRatio 71 | newX = 0.Coord 72 | newY = if newHeight > r.height: 0.Coord else: r.height / 2 - newHeight / 2 73 | c.drawImage(v.image, newRect(newX, newY, newWidth, newHeight)) 74 | of ImageFillRule.FitHeight: 75 | let 76 | stretchRatio = r.height / v.image.size.height 77 | newHeight = r.height 78 | newWidth = v.image.size.width * stretchRatio 79 | newY = 0.Coord 80 | newX = if newWidth > r.width: 0.Coord else: r.width / 2 - newWidth / 2 81 | c.drawImage(v.image, newRect(newX, newY, newWidth, newHeight)) 82 | of ImageFillRule.NinePartImage: 83 | c.drawNinePartImage(v.image, v.bounds, v.imageMarginLeft, v.imageMarginTop, v.imageMarginRight, v.imageMarginBottom) 84 | 85 | ImageView.properties: 86 | image 87 | fillRule 88 | imageMarginLeft 89 | imageMarginRight 90 | imageMarginTop 91 | imageMarginBottom 92 | 93 | registerClass(ImageView) 94 | genVisitorCodeForView(ImageView) 95 | genSerializeCodeForView(ImageView) 96 | -------------------------------------------------------------------------------- /nimx/inspector_panel.nim: -------------------------------------------------------------------------------- 1 | import nimx/text_field 2 | import nimx/scroll_view 3 | import nimx/panel_view 4 | import nimx/inspector_view 5 | 6 | export panel_view 7 | 8 | type InspectorPanel* = ref object of PanelView 9 | inspectorView: InspectorView 10 | mOnPropertyChanged: proc(name: string) {.gcsafe.} 11 | 12 | 13 | proc moveSubviewToBack(v, s: View) = 14 | let i = v.subviews.find(s) 15 | if i != -1: 16 | v.subviews.delete(i) 17 | v.subviews.insert(s, 0) 18 | 19 | proc onPropertyChanged*(i: InspectorPanel, cb: proc(name: string) {.gcsafe.}) = 20 | i.mOnPropertyChanged = cb 21 | 22 | method init*(i: InspectorPanel, r: Rect) = 23 | i.draggable = false 24 | procCall i.PanelView.init(r) 25 | i.collapsible = true 26 | i.collapsed = true 27 | let title = newLabel(newRect(22, 6, 96, 15)) 28 | title.textColor = whiteColor() 29 | title.text = "Properties" 30 | i.addSubview(title) 31 | i.autoresizingMask = { afFlexibleMaxX } 32 | i.inspectorView = InspectorView.new(newRect(0, i.titleHeight, r.width, r.height - i.titleHeight)) 33 | i.inspectorView.autoresizingMask = {afFlexibleWidth, afFlexibleMaxY} 34 | i.inspectorView.onPropertyChanged = proc(name: string) {.gcsafe.} = 35 | if not i.mOnPropertyChanged.isNil: 36 | i.mOnPropertyChanged(name) 37 | 38 | let sv = newScrollView(i.inspectorView) 39 | sv.horizontalScrollBar = nil 40 | sv.autoresizingMask = {afFlexibleWidth, afFlexibleHeight} 41 | sv.setFrameOrigin(newPoint(sv.frame.x, i.titleHeight)) 42 | sv.setFrameSize(newSize(i.frame.width, i.contentHeight)) 43 | 44 | i.addSubview(sv) 45 | i.moveSubviewToBack(sv) 46 | 47 | proc setInspectedObject*[T](i: InspectorPanel, o: T) {.inline.} = 48 | i.inspectorView.setInspectedObject(o) 49 | i.collapsed = o.isNil 50 | i.contentHeight = i.inspectorView.frame.height 51 | -------------------------------------------------------------------------------- /nimx/inspector_view.nim: -------------------------------------------------------------------------------- 1 | import nimx/linear_layout 2 | import nimx/property_visitor 3 | import nimx/property_editors/propedit_registry 4 | import nimx/property_editors/standard_editors 5 | 6 | import variant 7 | 8 | export linear_layout 9 | 10 | type InspectorView* = ref object of LinearLayout 11 | onPropertyChanged*: proc(name: string) {.gcsafe.} 12 | 13 | method init*(v: InspectorView, r: Rect) = 14 | procCall v.LinearLayout.init(r) 15 | v.horizontal = false 16 | 17 | proc setInspectedObject*[T](v: InspectorView, o: T) = 18 | v.removeAllSubviews() 19 | if o.isNil: return 20 | 21 | proc onChanged(p: PropertyVisitor): proc() {.gcsafe.} = 22 | let name = p.name 23 | result = proc() {.gcsafe.} = 24 | if not v.onPropertyChanged.isNil: 25 | v.onPropertyChanged(name) 26 | 27 | let oo = newVariant(o) 28 | var visitor : PropertyVisitor 29 | visitor.requireName = true 30 | visitor.requireSetter = true 31 | visitor.requireGetter = true 32 | visitor.flags = { pfEditable } 33 | visitor.commit = proc() {.gcsafe.} = 34 | v.addSubview(propertyEditorForProperty(oo, visitor.name, visitor.setterAndGetter, onChange = onChanged(visitor))) 35 | 36 | o.visitProperties(visitor) 37 | -------------------------------------------------------------------------------- /nimx/key_commands.nim: -------------------------------------------------------------------------------- 1 | import event, keyboard 2 | import private/js_platform_detector 3 | 4 | type KeyCommand* = enum 5 | kcUnknown 6 | kcCopy 7 | kcCut 8 | kcPaste 9 | kcDelete 10 | kcUseSelectionForFind 11 | kcSelectAll 12 | kcUndo 13 | kcRedo 14 | kcNew 15 | kcOpen 16 | kcSave 17 | kcSaveAs 18 | 19 | type Modifier = enum 20 | Shift 21 | Gui 22 | Ctrl 23 | Alt 24 | 25 | const web = defined(emscripten) or defined(js) 26 | 27 | template macOsCommands(body: untyped) = 28 | when defined(macosx): 29 | body 30 | elif web: 31 | if isMacOs: 32 | body 33 | 34 | template nonMacOsCommands(body: untyped) = 35 | when web: 36 | if not isMacOs: 37 | body 38 | elif not defined(macosx): 39 | body 40 | 41 | proc commandFromEvent*(e: Event): KeyCommand = 42 | if e.kind == etKeyboard and e.buttonState == bsDown: 43 | var curModifiers: set[Modifier] 44 | if e.modifiers.anyGui(): curModifiers.incl(Gui) 45 | if e.modifiers.anyShift(): curModifiers.incl(Shift) 46 | if e.modifiers.anyCtrl(): curModifiers.incl(Ctrl) 47 | if e.modifiers.anyAlt(): curModifiers.incl(Alt) 48 | 49 | template defineCmd(cmd: KeyCommand, vk: VirtualKey, modifiers: set[Modifier]) = 50 | if e.keyCode == vk and curModifiers == modifiers: return cmd 51 | 52 | macOsCommands: 53 | defineCmd kcUndo, VirtualKey.Z, {Gui} 54 | defineCmd kcRedo, VirtualKey.Z, {Shift, Gui} 55 | 56 | defineCmd kcCopy, VirtualKey.C, {Gui} 57 | defineCmd kcCut, VirtualKey.X, {Gui} 58 | defineCmd kcPaste, VirtualKey.V, {Gui} 59 | defineCmd kcUseSelectionForFind, VirtualKey.E, {Gui} 60 | 61 | defineCmd kcSelectAll, VirtualKey.A, {Gui} 62 | 63 | defineCmd kcNew, VirtualKey.N, {Gui} 64 | defineCmd kcOpen, VirtualKey.O, {Gui} 65 | defineCmd kcSave, VirtualKey.S, {Gui} 66 | defineCmd kcSaveAs, VirtualKey.S, {Shift, Gui} 67 | 68 | nonMacOsCommands: 69 | defineCmd kcUndo, VirtualKey.Z, {Ctrl} 70 | defineCmd kcRedo, VirtualKey.Y, {Ctrl} 71 | 72 | defineCmd kcCopy, VirtualKey.C, {Ctrl} 73 | defineCmd kcCut, VirtualKey.X, {Ctrl} 74 | defineCmd kcPaste, VirtualKey.V, {Ctrl} 75 | 76 | defineCmd kcSelectAll, VirtualKey.A, {Ctrl} 77 | 78 | defineCmd kcNew, VirtualKey.N, {Ctrl} 79 | defineCmd kcOpen, VirtualKey.O, {Ctrl} 80 | defineCmd kcSave, VirtualKey.S, {Ctrl} 81 | defineCmd kcSaveAs, VirtualKey.S, {Shift, Ctrl} 82 | 83 | defineCmd kcDelete, VirtualKey.Delete, {} 84 | -------------------------------------------------------------------------------- /nimx/layout_vars.nim: -------------------------------------------------------------------------------- 1 | import kiwi 2 | 3 | type 4 | LayoutVars* = object 5 | x*, y*, width*, height*: Variable 6 | 7 | proc centerX*(phs: LayoutVars): Expression = phs.x + phs.width / 2 8 | proc centerY*(phs: LayoutVars): Expression = phs.y + phs.height / 2 9 | 10 | proc left*(phs: LayoutVars): Variable {.inline.} = phs.x 11 | proc right*(phs: LayoutVars): Expression {.inline.} = phs.x + phs.width 12 | 13 | proc top*(phs: LayoutVars): Variable {.inline.} = phs.y 14 | proc bottom*(phs: LayoutVars): Expression = phs.y + phs.height 15 | 16 | const leftToRight = true 17 | 18 | proc leading*(phs: LayoutVars): Expression = 19 | if leftToRight: newExpression(phs.left) else: -phs.right 20 | 21 | proc trailing*(phs: LayoutVars): Expression = 22 | if leftToRight: phs.right else: -newExpression(phs.left) 23 | 24 | proc origin*(phs: LayoutVars): array[2, Expression] = [newExpression(phs.x), newExpression(phs.y)] 25 | proc center*(phs: LayoutVars): array[2, Expression] = [phs.centerX, phs.centerY] 26 | proc size*(phs: LayoutVars): array[2, Expression] = [newExpression(phs.width), newExpression(phs.height)] 27 | 28 | proc topLeading*(phs: LayoutVars): array[2, Expression] = [phs.leading, newExpression(phs.y)] 29 | proc bottomTrailing*(phs: LayoutVars): array[2, Expression] = [phs.trailing, phs.bottom] 30 | 31 | proc frame*(phs: LayoutVars): array[4, Expression] = [newExpression(phs.x), newExpression(phs.y), newExpression(phs.width), newExpression(phs.height)] 32 | 33 | proc inset*(fr: array[4, Expression], left, top, right, bottom: float32): array[4, Expression] = 34 | [fr[0] + left, fr[1] + top, fr[2] - (left + right), fr[3] - (top + bottom)] 35 | 36 | proc inset*(fr: array[4, Expression], byX, byY: float32): array[4, Expression] {.inline.} = 37 | inset(fr, byX, byY, byX, byY) 38 | 39 | proc inset*(fr: array[4, Expression], by: float32): array[4, Expression] {.inline.} = inset(fr, by, by) 40 | 41 | var prevPHS* {.threadvar.}, nextPHS* {.threadvar.}, superPHS* {.threadvar.}, selfPHS* {.threadvar}: LayoutVars 42 | 43 | proc isNan(f: float32): bool {.inline.} = f != f 44 | 45 | proc assertARDimentions(a, b, c: float32) = 46 | # Verifies that exactly one of the dimensions is NaN 47 | var i = 0 48 | if a.isNan: inc i 49 | if b.isNan: inc i 50 | if c.isNan: inc i 51 | assert(i == 1, "Exactly one of the dimensions must be NaN") 52 | 53 | proc autoresizingFrame*(leading, width, trailing, top, height, bottom: float32): array[4, Expression] = 54 | assertARDimentions(leading, width, trailing) 55 | assertARDimentions(top, height, bottom) 56 | 57 | if leading.isNan: 58 | result[0] = superPHS.trailing - selfPHS.width - trailing 59 | result[2] = newExpression(width) 60 | elif width.isNan: 61 | result[0] = superPHS.leading + leading 62 | result[2] = superPHS.trailing - selfPHS.leading - trailing 63 | else: # trailing.isNan 64 | result[0] = superPHS.leading + leading 65 | result[2] = newExpression(width) 66 | 67 | if top.isNan: 68 | result[1] = superPHS.bottom - selfPHS.height - bottom 69 | result[3] = newExpression(height) 70 | elif height.isNan: 71 | result[1] = superPHS.top + top 72 | result[3] = superPHS.bottom - selfPHS.top - bottom 73 | else: # bottom.isNan 74 | result[1] = superPHS.top + top 75 | result[3] = newExpression(height) 76 | 77 | proc init*(phs: var LayoutVars) = 78 | phs.x = newVariable("x", 0) 79 | phs.y = newVariable("y", 0) 80 | phs.width = newVariable("width", 0) 81 | phs.height = newVariable("height", 0) 82 | 83 | init(prevPHS) 84 | init(nextPHS) 85 | init(superPHS) 86 | init(selfPHS) 87 | -------------------------------------------------------------------------------- /nimx/meta_extensions/property_desc.nim: -------------------------------------------------------------------------------- 1 | import macros, tables, typetraits 2 | import nimx/class_registry 3 | 4 | export class_registry 5 | 6 | type 7 | PropertyDesc* = tuple 8 | name: string 9 | attributes: Table[string, NimNode] 10 | 11 | var props {.compileTime.} = initTable[string, seq[PropertyDesc]]() 12 | 13 | proc hasAttr*(d: PropertyDesc, attr: string): bool = 14 | attr in d.attributes 15 | 16 | iterator propertyDescs*(typdesc: NimNode): PropertyDesc = 17 | let k = $typdesc 18 | if k in props: 19 | for p in props[k]: 20 | yield p 21 | 22 | proc addPropertyAttr(d: var PropertyDesc, a: NimNode) = 23 | case a.kind 24 | of nnkIdent: 25 | d.attributes[$a] = nil 26 | of nnkExprEqExpr: 27 | a[0].expectKind(nnkIdent) 28 | d.attributes[$a[0]] = a[1] 29 | of nnkStmtList: 30 | for c in a: addPropertyAttr(d, c) 31 | of nnkCall: 32 | a[0].expectKind(nnkIdent) 33 | assert(a.len == 2) 34 | var b = a[1] 35 | if b.kind == nnkStmtList: 36 | assert(b.len == 1) 37 | b = b[0] 38 | d.attributes[$a[0]] = b 39 | of nnkProcDef: 40 | d.attributes[$a.name] = a 41 | else: 42 | echo "Unexpected attr kind: ", treeRepr(a) 43 | assert(false) 44 | 45 | proc parsePropertyDescs(properties: NimNode): seq[PropertyDesc] = 46 | result = @[] 47 | for p in properties: 48 | case p.kind 49 | of nnkStmtList: 50 | for c in p: result.add(parsePropertyDescs(c)) 51 | of nnkIdent: 52 | var pd: PropertyDesc 53 | pd.name = $p 54 | pd.attributes = initTable[string, NimNode]() 55 | result.add(pd) 56 | of nnkCall: 57 | var pd: PropertyDesc 58 | pd.name = $p[0] 59 | pd.attributes = initTable[string, NimNode]() 60 | for i in 1 ..< p.len: 61 | addPropertyAttr(pd, p[i]) 62 | result.add(pd) 63 | of nnkDiscardStmt: 64 | discard 65 | else: 66 | echo "Unexpected property desc kind: ", treeRepr(p) 67 | assert(false) 68 | 69 | proc inheritFrom*(n: NimNode): NimNode {.compileTime.}= 70 | let impl = n.getImpl 71 | var inherit: NimNode 72 | if impl[2].kind == nnkRefTy: 73 | inherit = impl[2][0][1] 74 | 75 | if impl[2].kind == nnkObjectTy: 76 | inherit = impl[2][1] 77 | 78 | if inherit.kind == nnkOfInherit: 79 | return inherit[0] 80 | 81 | error "Unknown type " & $n 82 | 83 | macro properties*(typdesc: typed{nkSym}, body: untyped): untyped = 84 | let k = $typdesc 85 | assert(k notin props) 86 | props[k] = parsePropertyDescs(body) 87 | -------------------------------------------------------------------------------- /nimx/meta_extensions/serializers_gen.nim: -------------------------------------------------------------------------------- 1 | import nimx / meta_extensions / property_desc 2 | import nimx / ui_resource 3 | export ui_resource 4 | import macros 5 | 6 | proc genSerializeCall(view, serializer, field: NimNode, isSerialize: bool): NimNode {.compileTime.}= 7 | let call = if isSerialize: ident("serialize") else: ident("deserialize") 8 | let fieldLit = newLit($field) 9 | # if isSerialize: 10 | result = quote do: 11 | `serializer`.`call`(`fieldLit`, `view`.`field`) 12 | # else: 13 | # result = quote do: 14 | # `serializer`.`call`(`view`.`field`) 15 | 16 | # echo "genSerializeCall ", repr(result) 17 | 18 | macro genSerializers(typdesc: typed{nkSym}): untyped= 19 | result = nnkStmtList.newNimNode() 20 | 21 | let viewArg = ident("v") 22 | let serArg = ident("s") 23 | 24 | var serializerBody = nnkStmtList.newNimNode() 25 | var deserializerBody = nnkStmtList.newNimNode() 26 | let parent = typdesc.inheritFrom() 27 | if parent.isNil: 28 | # echo "no inheritance " 29 | discard 30 | else: 31 | # echo "impl ", treeRepr(parent), " \ninherit from ", $parent 32 | serializerBody.add quote do: 33 | procCall `viewArg`.`parent`.serializeFields(`serArg`) 34 | 35 | deserializerBody.add quote do: 36 | procCall `viewArg`.`parent`.deserializeFields(`serArg`) 37 | 38 | for p in typdesc.propertyDescs(): 39 | let serCall = genSerializeCall(viewArg, serArg, ident(p.name), true) 40 | let desCall = genSerializeCall(viewArg, serArg, ident(p.name), false) 41 | serializerBody.add quote do: 42 | `serCall` 43 | deserializerBody.add quote do: 44 | `desCall` 45 | 46 | result.add quote do: 47 | method serializeFields*(`viewArg`: `typdesc`, `serArg`: Serializer) {.gcsafe.} = 48 | `serializerBody` 49 | 50 | method deserializeFields*(`viewArg`: `typdesc`, `serArg`: Deserializer) {.gcsafe.}= 51 | `deserializerBody` 52 | 53 | template genSerializeCodeForView*(c: typed) = 54 | import nimx / serializers 55 | 56 | genSerializers(c) 57 | -------------------------------------------------------------------------------- /nimx/meta_extensions/visitors_gen.nim: -------------------------------------------------------------------------------- 1 | import nimx / meta_extensions / property_desc 2 | import macros 3 | 4 | macro genVisitorProc(typdesc: typed{nkSym}): untyped= 5 | result = newNimNode(nnkStmtList) 6 | 7 | let visitorIdent = ident("pv") 8 | let viewIdent = ident("v") 9 | var visitBody = newNimNode(nnkStmtList) 10 | let parent = typdesc.inheritFrom() 11 | if not parent.isNil: 12 | visitBody.add quote do: 13 | procCall `viewIdent`.`parent`.visitProperties(`visitorIdent`) 14 | 15 | for p in typdesc.propertyDescs(): 16 | let plit = newLit(p.name) 17 | let pname = ident(p.name) 18 | visitBody.add quote do: 19 | `visitorIdent`.visitProperty(`plit`, `viewIdent`.`pname`) 20 | 21 | result.add quote do: 22 | method visitProperties*(`viewIdent`: `typdesc`, `visitorIdent`: var PropertyVisitor)= 23 | `visitBody` 24 | 25 | # echo "getVisitor result:\n", repr(result) 26 | 27 | template genVisitorCodeForView*(c: typed)= 28 | import nimx / property_visitor 29 | genVisitorProc(c) 30 | -------------------------------------------------------------------------------- /nimx/mini_profiler.nim: -------------------------------------------------------------------------------- 1 | import tables 2 | 3 | type 4 | ProfilerDataSourceBase {.inheritable, pure.} = ref object 5 | stringifiedValue: string 6 | updateImpl: proc(ds: ProfilerDataSourceBase) {.nimcall, gcsafe.} 7 | isDirty: bool 8 | 9 | ProfilerDataSource*[T] = ref object of ProfilerDataSourceBase 10 | mValue: T 11 | 12 | Profiler* = ref object 13 | values: Table[string, ProfilerDataSourceBase] 14 | enabled*: bool 15 | 16 | proc updateDataSource[T](ds: ProfilerDataSourceBase) {.nimcall.} = 17 | let ds = cast[ProfilerDataSource[T]](ds) 18 | ds.stringifiedValue = $ds.mValue 19 | ds.isDirty = false 20 | 21 | var gProfiler {.threadvar.}: Profiler 22 | 23 | proc newProfiler*(): Profiler = 24 | result.new() 25 | result.values = initTable[string, ProfilerDataSourceBase]() 26 | when defined(miniProfiler): 27 | result.enabled = true 28 | 29 | proc newDataSource*(p: Profiler, typ: typedesc, name: string): ProfilerDataSource[typ] = 30 | result.new() 31 | result.stringifiedValue = "" 32 | type TT = typ 33 | result.updateImpl = updateDataSource[TT] 34 | p.values[name] = result 35 | 36 | proc sharedProfiler*(): Profiler = 37 | if gProfiler.isNil: 38 | gProfiler = newProfiler() 39 | result = gProfiler 40 | 41 | proc setValueForKey*(p: Profiler, key, value: string) = 42 | var ds = p.values.getOrDefault(key) 43 | if ds.isNil: 44 | ds.new() 45 | p.values[key] = ds 46 | ds.stringifiedValue = value 47 | 48 | template `[]=`*(p: Profiler, key: string, value: typed) = 49 | p.setValueForKey(key, $value) 50 | 51 | proc valueForKey*(p: Profiler, key: string): string = 52 | let v = p.values.getOrDefault(key) 53 | if not v.isNil: 54 | if v.isDirty: v.updateImpl(v) 55 | result = v.stringifiedValue 56 | 57 | iterator pairs*(p: Profiler): tuple[key, value: string] = 58 | for k, v in p.values: 59 | if v.isDirty: v.updateImpl(v) 60 | yield (k, v.stringifiedValue) 61 | 62 | template len*(p: Profiler): int = p.values.len 63 | 64 | template `value=`*[T](ds: ProfilerDataSource[T], v: T) = 65 | ds.mValue = v 66 | ds.isDirty = true 67 | 68 | template value*[T](ds: ProfilerDataSource[T]): T = ds.mValue 69 | 70 | template inc*(ds: ProfilerDataSource[int]) = 71 | inc ds.mValue 72 | ds.isDirty = true 73 | 74 | template dec*(ds: ProfilerDataSource[int]) = 75 | dec ds.mValue 76 | ds.isDirty = true 77 | -------------------------------------------------------------------------------- /nimx/numeric_text_field.nim: -------------------------------------------------------------------------------- 1 | import strutils, math, times 2 | import nimx / [keyboard, text_field, formatted_text, 3 | view_event_handling, window_event_handling, composition, 4 | context, font, animation, window] 5 | 6 | type NumericTextField* = ref object of TextField 7 | precision*: uint 8 | initialMouseTime: float 9 | initialMouseX: Coord 10 | mouseX: Coord 11 | touchAnim: Animation 12 | directionLeft: bool 13 | 14 | proc newNumericTextField*(r: Rect, precision: uint = 2): NumericTextField = 15 | result.new() 16 | result.init(r) 17 | result.precision = precision 18 | 19 | method init*(v: NumericTextField, r: Rect) = 20 | procCall v.TextField.init(r) 21 | v.precision = 2 22 | v.formattedText.horizontalAlignment = haCenter 23 | 24 | #[ 25 | method onScroll*(v: NumericTextField, e: var Event): bool = 26 | result = true 27 | var action = false 28 | try: 29 | var val = parseFloat(v.text) 30 | if VirtualKey.LeftControl in e.modifiers: 31 | val += e.offset.y * 0.1 32 | elif VirtualKey.LeftShift in e.modifiers: 33 | val += e.offset.y * 10 34 | else: 35 | val += e.offset.y 36 | v.text = formatFloat(val, ffDecimal, v.precision) 37 | action = true 38 | v.setNeedsDisplay() 39 | except: 40 | discard 41 | if action: 42 | v.sendAction() 43 | ]# 44 | 45 | const arrowComposition = newComposition """ 46 | uniform float uAngle; 47 | 48 | void compose() { 49 | vec2 center = vec2(bounds.x + bounds.z / 2.0, bounds.y + bounds.w / 2.0 - 1.0); 50 | float triangle = sdRegularPolygon(center, 4.0, 3, uAngle); 51 | drawShape(triangle, vec4(0.7, 0.7, 0.7, 1)); 52 | } 53 | """ 54 | 55 | proc drawArrows(v: NumericTextField) = 56 | const arrowMargin = 10 57 | arrowComposition.draw newRect(0, 0, arrowMargin, v.bounds.height): 58 | setUniform("uAngle", Coord(PI)) 59 | arrowComposition.draw newRect(v.bounds.width - arrowMargin, 0, arrowMargin, v.bounds.height): 60 | setUniform("uAngle", Coord(0)) 61 | 62 | method draw*(t: NumericTextField, r: Rect) = 63 | procCall t.TextField.draw(r) 64 | if not t.isFirstResponder(): 65 | t.drawArrows() 66 | 67 | #[ 68 | proc roundTo(v, t: float): float = 69 | let vv = abs(v) 70 | let m = vv mod t 71 | if m > t / 2: 72 | result = vv + (t - m) 73 | else: 74 | result = vv - m 75 | if v < 0: result = -result 76 | ]# 77 | 78 | method onTouchEv*(t: NumericTextField, e: var Event): bool = 79 | if t.isFirstResponder(): 80 | return procCall t.TextField.onTouchEv(e) 81 | 82 | case e.buttonState 83 | of bsDown: 84 | t.initialMouseTime = epochTime() 85 | 86 | t.initialMouseX = e.localPosition.x 87 | t.mouseX = t.initialMouseX 88 | 89 | var val = try: parseFloat(t.text) 90 | except: 0.0 91 | 92 | t.touchAnim = newAnimation() 93 | t.touchAnim.loopDuration = 1.0 94 | t.touchAnim.numberOfLoops = -1 95 | t.touchAnim.onAnimate = proc(p: float)= 96 | let diff = t.initialMouseX - t.mouseX 97 | let absDiff = abs(diff) 98 | if absDiff > 0.0: 99 | val = val - (diff) / (10000.0 / absDiff) 100 | t.text = formatFloat(val, ffDecimal, t.precision) 101 | t.sendAction() 102 | 103 | t.window.addAnimation(t.touchAnim) 104 | 105 | result = true 106 | 107 | of bsUp: 108 | if epochTime() - t.initialMouseTime < 0.3: 109 | result = t.makeFirstResponder() 110 | 111 | t.mouseX = t.initialMouseX 112 | t.touchAnim.cancel() 113 | t.touchAnim = nil 114 | 115 | of bsUnknown: 116 | var direction = t.mouseX > e.localPosition.x 117 | if direction != t.directionLeft: 118 | t.directionLeft = direction 119 | t.initialMouseX = e.localPosition.x 120 | t.mouseX = e.localPosition.x 121 | result = true 122 | -------------------------------------------------------------------------------- /nimx/panel_view.nim: -------------------------------------------------------------------------------- 1 | import math 2 | import nimx/view 3 | import nimx/app 4 | import nimx/event 5 | import nimx/context 6 | import nimx/types 7 | import nimx/composition 8 | import nimx/gesture_detector 9 | import nimx/view 10 | import view_dragging_listener 11 | import nimx / meta_extensions / [ property_desc, visitors_gen, serializers_gen ] 12 | 13 | type PanelView* = ref object of View 14 | draggable*: bool 15 | collapsible*: bool 16 | mCollapsed: bool 17 | contentHeight*: Coord 18 | 19 | template titleHeight*(v: PanelView): Coord = Coord(27) 20 | 21 | proc `collapsed=`*(v: PanelView, f: bool) = 22 | v.mCollapsed = f 23 | v.setFrameSize(newSize(v.frame.size.width, if v.mCollapsed: v.titleHeight else: v.titleHeight + v.contentHeight)) 24 | v.setNeedsDisplay() 25 | 26 | template collapsed*(v: PanelView): bool = v.mCollapsed 27 | 28 | # PanelView implementation 29 | 30 | method init*(v: PanelView, r: Rect) = 31 | procCall v.View.init(r) 32 | v.backgroundColor = newColor(0.5, 0.5, 0.5, 0.5) 33 | v.mCollapsed = false 34 | v.collapsible = false 35 | v.contentHeight = r.height - v.titleHeight 36 | 37 | if v.draggable: 38 | v.enableDraggingByBackground() 39 | v.enableViewResizing() 40 | 41 | # Enable collapsibility 42 | v.addGestureDetector(newTapGestureDetector(proc(tapPoint: Point) = 43 | let innerPoint = tapPoint - v.frame.origin 44 | if innerPoint.x > 0 and innerPoint.x < v.titleHeight and innerPoint.y > 0 and innerPoint.y < v.titleHeight: 45 | if v.collapsible: 46 | v.collapsed = not v.collapsed 47 | )) 48 | 49 | const gradientComposition = newComposition """ 50 | void compose() { 51 | vec4 color = gradient( 52 | smoothstep(bounds.y, 27.0, vPos.y), 53 | newGrayColor(0.5), 54 | newGrayColor(0.1) 55 | ); 56 | drawShape(sdRoundedRect(bounds, 6.0), color); 57 | } 58 | """ 59 | 60 | method draw(v: PanelView, r: Rect) = 61 | # Draws Panel View 62 | let c = currentContext() 63 | 64 | # Top label 65 | c.fillColor = newGrayColor(0.05, 0.8) 66 | c.strokeColor = newGrayColor(0.05, 0.8) 67 | 68 | c.drawRoundedRect(newRect(r.x, r.y, r.width, r.height), 6) 69 | 70 | if v.collapsible: 71 | var disclosureTriangleAngle: Coord 72 | if not v.collapsed: 73 | # Main panel 74 | c.fillColor = newGrayColor(0.4, 0.6) 75 | c.strokeColor = newGrayColor(0.4, 0.6) 76 | c.drawRect(newRect(r.x, r.y + v.titleHeight, r.width, r.height - v.titleHeight)) 77 | else: 78 | disclosureTriangleAngle = Coord(PI / 2.0) 79 | 80 | c.fillColor = newColor(0.7, 0.7, 0.7) 81 | c.drawTriangle(newRect(r.x, r.y, v.titleHeight, v.titleHeight), disclosureTriangleAngle) 82 | 83 | method clipType*(v: PanelView): ClipType = ctDefaultClip 84 | 85 | PanelView.properties: 86 | collapsible 87 | contentHeight 88 | 89 | registerClass(PanelView) 90 | genVisitorCodeForView(PanelView) 91 | genSerializeCodeForView(PanelView) 92 | -------------------------------------------------------------------------------- /nimx/pasteboard/abstract_pasteboard.nim: -------------------------------------------------------------------------------- 1 | ## Pasteboards 2 | ## Writing a string to pasteboard 3 | ## let p = pasteboardWithName(PboardGeneral) 4 | ## p.write(newPasteboardItem("Hello, world!")) 5 | ## 6 | ## Reading a string 7 | ## let myString = p.read().data 8 | 9 | import pasteboard_item 10 | export pasteboard_item 11 | 12 | type 13 | Pasteboard* {.inheritable.} = ref object 14 | writeImpl*: proc(pb: Pasteboard, pi: varargs[PasteboardItem] ) {.nimcall, gcsafe.} 15 | readImpl*: proc(pb: Pasteboard, kind: string): PasteboardItem {.nimcall, gcsafe.} 16 | 17 | const PboardGeneral* = "__nimx.PboardGeneral" 18 | const PboardFont* = "__nimx.PboardFont" 19 | const PboardRuler* = "__nimx.PboardRuler" 20 | const PboardFind* = "__nimx.PboardFind" 21 | const PboardDrag* = "__nimx.PboardDrag" 22 | 23 | proc write*(pb: Pasteboard, pi: varargs[PasteboardItem]) {.inline.} = 24 | if not pb.writeImpl.isNil: pb.writeImpl(pb, pi) 25 | proc read*(pb: Pasteboard, kind: string): PasteboardItem {.inline.} = 26 | if not pb.readImpl.isNil: result = pb.readImpl(pb, kind) 27 | 28 | proc writeString*(pb: Pasteboard, s: string) = pb.write(newPasteboardItem(s)) 29 | proc readString*(pb: Pasteboard): string = 30 | let pi = pb.read(PboardKindString) 31 | if not pi.isNil and pi.kind == PboardKindString: 32 | result = pi.data 33 | -------------------------------------------------------------------------------- /nimx/pasteboard/pasteboard.nim: -------------------------------------------------------------------------------- 1 | when defined(macosx) and not defined(ios): 2 | import pasteboard_mac 3 | export pasteboard_mac 4 | 5 | elif defined(windows): 6 | import pasteboard_win 7 | export pasteboard_win 8 | 9 | elif defined(js) or defined(emscripten) or defined(wasm): 10 | import pasteboard_web 11 | export pasteboard_web 12 | 13 | elif defined(linux) and not defined(android): 14 | import pasteboard_x11 15 | export pasteboard_x11 16 | 17 | else: 18 | import abstract_pasteboard 19 | export abstract_pasteboard 20 | proc pasteboardWithName*(name: string): Pasteboard = result.new() 21 | 22 | 23 | when isMainModule: 24 | # Some tests... 25 | let pb = pasteboardWithName(PboardGeneral) 26 | pb.writeString("Hello world!") 27 | let s = pb.readString() 28 | echo s 29 | -------------------------------------------------------------------------------- /nimx/pasteboard/pasteboard_item.nim: -------------------------------------------------------------------------------- 1 | 2 | type PasteboardItem* = ref object 3 | kind*: string 4 | data*: string 5 | 6 | const PboardKindString* = "string" 7 | 8 | proc newPasteboardItem*(kind, data: string): PasteboardItem = 9 | result.new() 10 | result.kind = kind 11 | result.data = data 12 | 13 | proc newPasteboardItem*(s: string): PasteboardItem = newPasteboardItem(PboardKindString, s) 14 | -------------------------------------------------------------------------------- /nimx/pasteboard/pasteboard_mac.nim: -------------------------------------------------------------------------------- 1 | import abstract_pasteboard 2 | export abstract_pasteboard 3 | import pasteboard_item 4 | 5 | import darwin/app_kit 6 | 7 | type MacPasteboard = ref object of Pasteboard 8 | p: NSPasteboard 9 | 10 | proc finalizePboard(p: MacPasteboard) = p.p.release() 11 | 12 | proc nativePboardName(n: string): NSString = 13 | case n 14 | of PboardGeneral: result = NSGeneralPboard 15 | of PboardFont: result = NSFontPboard 16 | of PboardRuler: result = NSRulerPboard 17 | of PboardFind: result = NSFindPboard 18 | of PboardDrag: result = NSDragPboard 19 | else: result = n 20 | 21 | proc kindToNative(k: string): NSString = 22 | case k 23 | of PboardKindString: result = NSPasteboardTypeString 24 | else: result = k 25 | 26 | proc pbWrite(p: Pasteboard, pi_ar: varargs[PasteboardItem]) = 27 | let pb = MacPasteboard(p) 28 | pb.p.clearContents() 29 | let items = newMutableArray[NSPasteboardItem]() 30 | for pi in pi_ar: 31 | let npi = NSPasteboardItem.alloc().init() 32 | let data = dataWithBytes(addr pi.data[0], pi.data.len) 33 | discard npi.setDataForType(data, kindToNative(pi.kind)) 34 | items.add(npi) 35 | npi.release() 36 | pb.p.writeObjects(items) 37 | items.release() 38 | 39 | proc pbRead(p: Pasteboard, kind: string): PasteboardItem = 40 | let pb = MacPasteboard(p) 41 | let typ = kindToNative(kind) 42 | let d = pb.p.dataForType(typ) 43 | if not d.isNil: 44 | result.new() 45 | result.kind = kind 46 | let ln = d.length 47 | result.data = newString(ln) 48 | d.getBytes(addr result.data[0], ln) 49 | 50 | proc pasteboardWithName*(name: string): Pasteboard = 51 | var res: MacPasteboard 52 | res.new(finalizePboard) 53 | res.p = NSPasteboard.withName(nativePboardName(name)).retain() 54 | res.writeImpl = pbWrite 55 | res.readImpl = pbRead 56 | result = res 57 | -------------------------------------------------------------------------------- /nimx/pasteboard/pasteboard_web.nim: -------------------------------------------------------------------------------- 1 | import abstract_pasteboard 2 | export abstract_pasteboard 3 | import pasteboard_item 4 | 5 | import strutils 6 | import jsbind 7 | 8 | type WebPasteboard = ref object of Pasteboard 9 | 10 | const webCode = """ 11 | var textArea = document.createElement("textarea"); 12 | textArea.style.position = 'absolute'; 13 | textArea.style.top = "-200px"; 14 | textArea.style.left = "-200px"; 15 | textArea.style.width = '2px'; 16 | textArea.style.height = '2px'; 17 | textArea.value = $1; 18 | document.body.appendChild(textArea); 19 | textArea.select(); 20 | try { 21 | var successful = document.execCommand('copy'); 22 | } catch (err) { 23 | } 24 | document.body.removeChild(textArea); 25 | """ 26 | 27 | when defined(js): 28 | const jsCode = webCode.format("`cdata`") 29 | else: 30 | import jsbind/emscripten 31 | const emCode = webCode.format("UTF8ToString($0)") 32 | 33 | proc pbWrite(p: Pasteboard, pi_ar: varargs[PasteboardItem]) = 34 | let item = pi_ar[0] 35 | if item.kind == PboardKindString: 36 | when defined(js): 37 | let cdata: cstring = item.data 38 | {.emit: jsCode.} 39 | else: 40 | discard EM_ASM_INT(emCode, cstring(item.data)) 41 | 42 | proc pbRead(p: Pasteboard, kind: string): PasteboardItem = discard 43 | 44 | proc pasteboardWithName*(name: string): Pasteboard = 45 | var res: WebPasteboard 46 | res.new() 47 | res.writeImpl = pbWrite 48 | res.readImpl = pbRead 49 | result = res 50 | -------------------------------------------------------------------------------- /nimx/pasteboard/pasteboard_x11.nim: -------------------------------------------------------------------------------- 1 | import abstract_pasteboard 2 | export abstract_pasteboard 3 | import pasteboard_item 4 | import x11/[xlib, x, xatom] 5 | import nimx/app, nimx/private/windows/sdl_window 6 | import sdl2 7 | 8 | type X11Pasteboard = ref object of Pasteboard 9 | const XINT_MAX = 32767 10 | 11 | type WMinfoX11 = object 12 | version*: SDL_Version 13 | subsystem*: SysWMType 14 | display*: PDisplay 15 | window*: culong 16 | 17 | proc getTextFormat(d: PDisplay): TAtom = 18 | when defined(X_HAVE_UTF8_STRING): 19 | result = XInternAtom(d, "UTF8_STRING", 0) 20 | else: 21 | result = XA_STRING 22 | 23 | const x11ClipboardSelection = "CLIPBOARD" 24 | 25 | proc nimxCutBuffer(display: PDisplay): TAtom = 26 | result = XInternAtom(display, "SDL_CUTBUFFER", 0) 27 | 28 | proc displayConnection(): (PDisplay, TWindow, TWindow) = 29 | let keyWnd = mainApplication().keyWindow() 30 | if keyWnd.isNil: return 31 | 32 | var winInfo: WMinfo 33 | getVersion(winInfo.version) 34 | 35 | let res = keyWnd.SdlWindow.getSDLWindow().getWMInfo(winInfo) 36 | let wi = cast[ptr WMinfoX11](addr winInfo) 37 | let display = wi.display 38 | 39 | if res == False32 or winInfo.subsystem != SysWM_X11 or display.isNil: 40 | return 41 | 42 | var rw: TWindow 43 | {.gcsafe.}: 44 | rw = DefaultRootWindow(display) 45 | (display, wi.window, rw) 46 | 47 | proc pbWrite(p: Pasteboard, pi_ar: varargs[PasteboardItem]) = 48 | let (display, window, rootWindow) = displayConnection() 49 | if display.isNil: return 50 | 51 | var format = getTextFormat(display) 52 | let clipboard = XInternAtom(display, x11ClipboardSelection, 0) 53 | let cutBuffer = nimxCutBuffer(display) 54 | for pi in pi_ar: 55 | discard XChangeProperty(display, rootWindow, cutBuffer, format, 8.cint, PropModeReplace, pi.data.Pcuchar, pi.data.len.cint) 56 | if clipboard != None and XGetSelectionOwner(display, clipboard) != window: 57 | discard XSetSelectionOwner(display, clipboard, window, CurrentTime) 58 | if XGetSelectionOwner(display, XA_PRIMARY) != window: 59 | discard XSetSelectionOwner(display, XA_PRIMARY, window, CurrentTime) 60 | 61 | proc pbRead(p: Pasteboard, kind: string): PasteboardItem = 62 | let (display, window, rootWindow) = displayConnection() 63 | if display.isNil: return 64 | 65 | var format = getTextFormat(display) 66 | let clipboard = XInternAtom(display, x11ClipboardSelection, 0) 67 | let cutBuffer = nimxCutBuffer(display) 68 | var selection: TAtom 69 | var owner = XGetSelectionOwner(display, clipboard) 70 | 71 | if owner == None: 72 | owner = rootWindow 73 | selection = XA_CUT_BUFFER0 74 | format = XA_STRING 75 | 76 | elif owner == window: 77 | owner = rootWindow 78 | selection = cutBuffer 79 | else: 80 | owner = window 81 | selection = XInternAtom(display, "SDL_SELECTION", 0) 82 | discard XConvertSelection(display, clipboard, format, selection, owner, CurrentTime) 83 | 84 | var selType: TAtom 85 | var selFormat: cint 86 | var bytes: culong = 0 87 | var overflow: culong = 0 88 | var src : cstring 89 | 90 | if XGetWindowProperty(display, owner, selection, 0.clong, (XINT_MAX div 4).clong, 0.XBool, format, (addr selType).PAtom, 91 | (addr selFormat).PCint, (addr bytes).Pculong, (addr overflow).Pculong, cast[PPcuchar](addr src)) == Success: 92 | if selType == format: 93 | var data = $src 94 | result = newPasteboardItem(PboardKindString, data) 95 | discard XFree(src) 96 | 97 | proc pasteboardWithName*(name: string): Pasteboard= 98 | var res = new(X11Pasteboard) 99 | res.writeImpl = pbWrite 100 | res.readImpl = pbRead 101 | 102 | result = res 103 | -------------------------------------------------------------------------------- /nimx/perform_on_main_thread.nim: -------------------------------------------------------------------------------- 1 | 2 | when (defined(macosx) or defined(ios)) and defined(nimxAvoidSDL): 3 | import private/objc_appkit 4 | 5 | enableObjC() 6 | 7 | {.emit: """ 8 | #import 9 | 10 | @interface __NimxMainThreadExecutor__ : NSObject { 11 | @public 12 | void (*func)(void*); 13 | void* data; 14 | } 15 | @end 16 | 17 | @implementation __NimxMainThreadExecutor__ 18 | - (void) execute { 19 | func(data); 20 | [self release]; 21 | } 22 | @end 23 | """.} 24 | 25 | {.push stack_trace:off.} 26 | proc performOnMainThread*(fun: proc(data: pointer) {.cdecl.}, data: pointer): int {.discardable.} = 27 | {.emit: """ 28 | __NimxMainThreadExecutor__* executor = [[__NimxMainThreadExecutor__ alloc] init]; 29 | executor->func = `fun`; 30 | executor->data = `data`; 31 | [executor performSelectorOnMainThread: @selector(execute) withObject: nil waitUntilDone: NO]; 32 | """.} 33 | {.pop.} 34 | elif (defined(linux) or defined(windows)) and not defined(android) and defined(nimxAvoidSDL): 35 | import locks, asyncdispatch 36 | 37 | type 38 | Elem = object 39 | p: proc(data: pointer) {.cdecl.} 40 | d: pointer 41 | 42 | SharedArray[T] = object 43 | data: ptr UncheckedArray[T] 44 | len, cap: int 45 | lock: Lock 46 | 47 | proc init[T](s: var SharedArray[T]) = 48 | initLock(s.lock) 49 | 50 | {.push stackTrace: off.} 51 | proc add[T](s: var SharedArray[T], v: T) = 52 | acquire(s.lock) 53 | if s.cap == s.len: 54 | s.cap += 4 55 | s.data = cast[ptr UncheckedArray[T]](reallocShared(s.data, s.cap * sizeof(T))) 56 | s.data[s.len] = v 57 | inc s.len 58 | release(s.lock) 59 | {.pop.} 60 | 61 | var callbacks: SharedArray[(proc(data: pointer) {.cdecl.}, pointer)] 62 | callbacks.init() 63 | var event = newAsyncEvent() 64 | addEvent(event) do(f: AsyncFD) -> bool {.gcsafe.}: 65 | acquire(callbacks.lock) 66 | for i in 0 ..< callbacks.len: 67 | let e = callbacks.data[i] 68 | try: 69 | {.gcsafe.}: 70 | e[0](e[1]) 71 | except Exception as e: 72 | echo "Exception while performing callback on main thread: ", e.msg, ": ", e.getStackTrace() 73 | callbacks.len = 0 74 | release(callbacks.lock) 75 | 76 | {.push stack_trace:off.} 77 | proc performOnMainThread*(fun: proc(data: pointer) {.cdecl.}, data: pointer): int {.discardable.} = 78 | callbacks.add((fun, data)) 79 | event.trigger() 80 | {.pop.} 81 | 82 | else: 83 | import sdl2 84 | 85 | {.push stack_trace:off.} 86 | proc performOnMainThread*(fun: proc(data: pointer) {.cdecl.}, data: pointer): int {.discardable.} = 87 | var evt = UserEventObj(kind: UserEvent5) 88 | evt.data1 = fun 89 | evt.data2 = data 90 | result = pushEvent(cast[ptr Event](addr evt)) 91 | {.pop.} 92 | -------------------------------------------------------------------------------- /nimx/popup_button.nim: -------------------------------------------------------------------------------- 1 | import control 2 | export control 3 | import menu 4 | import composition 5 | import context 6 | import font 7 | import view_event_handling 8 | 9 | type PopupButton* = ref object of Control 10 | mItems: seq[MenuItem] 11 | mSelectedIndex: int 12 | 13 | proc newPopupButton(r: Rect): PopupButton = 14 | result.new() 15 | result.init(r) 16 | 17 | method init*(b: PopupButton, r: Rect) = 18 | procCall b.Control.init(r) 19 | b.mSelectedIndex = -1 20 | 21 | proc `items=`*(b: PopupButton, items: openarray[string]) = 22 | let ln = items.len 23 | b.mItems.setLen(ln) 24 | if b.mSelectedIndex > ln - 1: 25 | b.mSelectedIndex = ln - 1 26 | elif b.mSelectedIndex == -1 and ln > 0: 27 | b.mSelectedIndex = 0 28 | for i, item in items: 29 | let it = item 30 | closureScope: 31 | let ii = i 32 | b.mItems[ii] = newMenuItem(it) 33 | b.mItems[ii].action = proc() = 34 | b.mSelectedIndex = ii 35 | b.sendAction(Event(kind: etUnknown)) 36 | b.setNeedsDisplay() 37 | 38 | proc newPopupButton*(parent: View = nil, position: Point = newPoint(0, 0), size: Size = newSize(100, 20), items: openarray[string]=[], selectedIndex: int=0): PopupButton = 39 | result = newPopupButton(newRect(position.x, position.y, size.width, size.height)) 40 | result.mSelectedIndex = selectedIndex 41 | result.items = items 42 | if not isNil(parent): 43 | parent.addSubview(result) 44 | 45 | proc selectedIndex*(b: PopupButton): int = b.mSelectedIndex 46 | ## Returns selected item index 47 | 48 | proc selectedItem*(b: PopupButton): string = b.mItems[b.mSelectedIndex].title 49 | 50 | proc `selectedIndex=`*(b: PopupButton, index: int) = 51 | ## Set selected item manually 52 | b.mSelectedIndex = index 53 | b.setNeedsDisplay() 54 | 55 | const pbComposition = newComposition """ 56 | uniform vec4 uFillColorStart; 57 | uniform vec4 uFillColorEnd; 58 | 59 | float radius = 5.0; 60 | 61 | void compose() { 62 | float stroke = sdRoundedRect(bounds, radius); 63 | float fill = sdRoundedRect(insetRect(bounds, 1.0), radius - 1.0); 64 | float buttonWidth = 20.0; 65 | float textAreaWidth = bounds.z - buttonWidth; 66 | vec4 textAreaRect = bounds; 67 | textAreaRect.z = textAreaWidth; 68 | 69 | vec4 buttonRect = bounds; 70 | buttonRect.x += textAreaWidth; 71 | buttonRect.z = buttonWidth; 72 | drawShape(stroke, newGrayColor(0.78)); 73 | 74 | drawShape(sdAnd(fill, sdRect(textAreaRect)), newGrayColor(1.0)); 75 | 76 | vec4 buttonColor = gradient(smoothstep(bounds.y, bounds.y + bounds.w, vPos.y), 77 | uFillColorStart, 78 | uFillColorEnd); 79 | drawShape(sdAnd(fill, sdRect(buttonRect)), buttonColor); 80 | 81 | drawShape(sdRegularPolygon(vec2(buttonRect.x + buttonRect.z / 2.0, buttonRect.y + buttonRect.w / 2.0 - 1.0), 4.0, 3, PI/2.0), vec4(1.0)); 82 | } 83 | """ 84 | 85 | method draw(b: PopupButton, r: Rect) = 86 | pbComposition.draw b.bounds: 87 | setUniform("uFillColorStart", newColor(0.31, 0.60, 0.98)) 88 | setUniform("uFillColorEnd", newColor(0.09, 0.42, 0.88)) 89 | if b.mSelectedIndex >= 0 and b.mSelectedIndex < b.mItems.len: 90 | let c = currentContext() 91 | c.fillColor = blackColor() 92 | let font = systemFont() 93 | c.drawText(font, newPoint(4, b.bounds.y + (b.bounds.height - font.height) / 2), b.mItems[b.mSelectedIndex].title) 94 | 95 | method onTouchEv(b: PopupButton, e: var Event): bool = 96 | if b.mItems.len > 0: 97 | case e.buttonState 98 | of bsDown: 99 | var menu : MenuItem 100 | menu.new() 101 | menu.items = b.mItems 102 | menu.popupAtPoint(b, newPoint(0, -b.mSelectedIndex.Coord * 20.0), newSize(b.bounds.size.width, 20.0)) 103 | else: discard 104 | -------------------------------------------------------------------------------- /nimx/private/async.nim: -------------------------------------------------------------------------------- 1 | import std/async 2 | export async 3 | 4 | when defined(js): 5 | proc asyncCheck*[T](future: Future[T]) = 6 | # Exceptions are always raised the Web Promise API. 7 | discard 8 | -------------------------------------------------------------------------------- /nimx/private/font/font_data.nim: -------------------------------------------------------------------------------- 1 | 2 | const charChunkLength* = 200 3 | 4 | type GlyphMetricsComponent* = enum 5 | compX = 0 6 | compY 7 | compAdvance 8 | compTexX 9 | compTexY 10 | compWidth 11 | compHeight 12 | 13 | const numberOfComponents = ord(high(GlyphMetricsComponent)) + 1 14 | type GlyphMetrics* = array[numberOfComponents * charChunkLength, int16] 15 | 16 | template charOff*(i: int): int = i * numberOfComponents 17 | template charOffComp*(bc: var GlyphMetrics, charOffset: int, comp: GlyphMetricsComponent): var int16 = 18 | bc[charOffset + ord(comp)] 19 | 20 | type GlyphData* = object 21 | glyphMetrics*: GlyphMetrics 22 | bitmap*: seq[byte] 23 | dfDoneForGlyph*: seq[bool] 24 | bitmapWidth*, bitmapHeight*: uint16 25 | 26 | template isPrintableCodePoint*(c: int): bool = not (i <= 0x1f or i == 0x7f or (i >= 0x80 and i <= 0x9F)) 27 | -------------------------------------------------------------------------------- /nimx/private/helper_macros.nim: -------------------------------------------------------------------------------- 1 | import macros 2 | 3 | proc replaceIf*(inNode, withNode: NimNode, predicate: proc(n: NimNode): bool) = 4 | ## Recursively replaces all nodes matching `predicate` in `inNode` with 5 | ## `withNode`. 6 | var i = 0 7 | for c in inNode: 8 | if predicate(c): 9 | inNode[i] = withNode 10 | else: 11 | replaceIf(c, withNode, predicate) 12 | inc i 13 | 14 | macro staticFor*(cond: untyped, body: untyped): untyped = 15 | cond.expectKind(nnkInfix) 16 | assert($cond[0] == "in") 17 | cond[1].expectKind(nnkIdent) 18 | let counterName = $cond[1] 19 | 20 | result = newNimNode(nnkStmtList) 21 | if cond.len == 3 and cond[2].kind == nnkBracket: 22 | for c in cond[2]: 23 | let copiedBody = body.copyNimTree() 24 | copiedBody.replaceIf(c) do(n: NimNode) -> bool: 25 | result = n.kind == nnkIdent and $n == counterName 26 | result.add(copiedBody) 27 | else: 28 | for i in 2 ..< cond.len: 29 | let subject = cond[i] 30 | let copiedBody = body.copyNimTree() 31 | copiedBody.replaceIf(subject) do(n: NimNode) -> bool: 32 | result = n.kind == nnkIdent and $n == counterName 33 | result.add(copiedBody) 34 | 35 | when isMainModule: 36 | import typetraits 37 | 38 | proc testProc() = 39 | block: 40 | var s = newSeq[string]() 41 | staticFor t in [int, float]: 42 | s.add(t.name) 43 | doAssert(s.len == 2) 44 | doAssert(s[0] == "int") 45 | doAssert(s[1] == "float") 46 | 47 | block: 48 | var s = newSeq[string]() 49 | 50 | template doTheTest(args: varargs[untyped]) = 51 | staticFor t in args: 52 | s.add(t.name) 53 | 54 | doTheTest(int, float) 55 | doAssert(s.len == 2) 56 | doAssert(s[0] == "int") 57 | doAssert(s[1] == "float") 58 | 59 | testProc() 60 | -------------------------------------------------------------------------------- /nimx/private/image_pvr.nim: -------------------------------------------------------------------------------- 1 | 2 | type PVRTextureHeaderV3 {.packed.} = object 3 | version: uint32 4 | flags: uint32 5 | pixelFormat: uint64 6 | colourSpace: uint32 7 | channelType: uint32 8 | height: uint32 9 | width: uint32 10 | depth: uint32 11 | numSurfaces: uint32 12 | numFaces: uint32 13 | numMipmaps: uint32 14 | metaDataSize: uint32 15 | 16 | type ePVR3Format* = enum 17 | PVR3_PVRTC_2BPP_RGB = 0, 18 | PVR3_PVRTC_2BPP_RGBA = 1, 19 | PVR3_PVRTC_4BPP_RGB = 2, 20 | PVR3_PVRTC_4BPP_RGBA = 3, 21 | PVR3_PVRTC2_2BPP = 4, 22 | PVR3_PVRTC2_4BPP = 5, 23 | PVR3_ETC1 = 6, 24 | PVR3_DXT1_OR_BC1 = 7, 25 | PVR3_DXT2 = 8, 26 | PVR3_DXT3_OR_BC2 = 9, 27 | PVR3_DXT4 = 10, 28 | PVR3_DXT5_OR_BC3 = 11, 29 | PVR3_BC4 = 12, 30 | PVR3_BC5 = 13, 31 | PVR3_BC6 = 14, 32 | PVR3_BC7 = 15, 33 | PVR3_UYVY = 16, 34 | PVR3_YUY2 = 17, 35 | PVR3_BW_1BPP = 18, 36 | PVR3_R9G9B9E5 = 19, 37 | PVR3_RGBG8888 = 20, 38 | PVR3_GRGB8888 = 21, 39 | PVR3_ETC2_RGB = 22, 40 | PVR3_ETC2_RGBA = 23, 41 | PVR3_ETC2_RGB_A1 = 24, 42 | PVR3_EAC_R11_U = 25, 43 | PVR3_EAC_R11_S = 26, 44 | PVR3_EAC_RG11_U = 27, 45 | PVR3_EAC_RG11_S = 28, 46 | 47 | proc loadPVRDataToTexture(data: ptr uint8, texture: var TextureRef, size: var Size, texCoords: var array[4, GLfloat]) = 48 | let header = cast[ptr PVRTextureHeaderV3](data) 49 | 50 | texCoords[2] = 1.0 51 | texCoords[3] = 1.0 52 | 53 | # dimensions 54 | let width = header.width 55 | let height = header.height 56 | #self.size = CGSizeMake((float)width/self.scale, (float)height/self.scale); 57 | #self.textureSize = CGSizeMake(width, height); 58 | #self.clipRect = CGRectMake(0.0f, 0.0f, width, height); 59 | #self.contentRect = CGRectMake(0.0f, 0.0f, self.size.width, self.size.height); 60 | 61 | # used for caching 62 | #self.cost = [data length] - header->headerLength; 63 | 64 | #alpha 65 | #self.premultipliedAlpha = YES; 66 | 67 | size.width = Coord(width) 68 | size.height = Coord(height) 69 | 70 | # format 71 | var compressed = false 72 | var typ: GLenum 73 | var format: GLenum 74 | var bpp: GLsizei 75 | 76 | let pf = ePVR3Format(header.pixelFormat and 0xff) 77 | case pf 78 | of PVR3_ETC2_RGBA: 79 | compressed = true 80 | format = GL_COMPRESSED_RGBA8_ETC2_EAC 81 | bpp = 8 82 | # typ = GL_UNSIGNED_SHORT_4_4_4_4 83 | else: 84 | raise newException(Exception, "Unsupported format: " & $pf) 85 | 86 | # create texture 87 | glGenTextures(1, addr texture) 88 | glBindTexture(GL_TEXTURE_2D, texture) 89 | let filter = GLint(if header.numMipmaps == 1: GL_LINEAR else: GL_LINEAR_MIPMAP_LINEAR) 90 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter) 91 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR) 92 | var offset = sizeof(PVRTextureHeaderV3).uint + header.metaDataSize.uint 93 | let nm = header.numMipmaps 94 | var i = 0'u32 95 | while i < nm: 96 | let mipmapWidth = GLsizei(width shr i) 97 | let mipmapHeight = GLsizei(height shr i) 98 | var pixelBytes = GLsizei(mipmapWidth * mipmapHeight * bpp / 8) 99 | let pImageData = cast[pointer](cast[uint](data) + offset) 100 | if compressed: 101 | pixelBytes = max(32, pixelBytes); 102 | glCompressedTexImage2D(GL_TEXTURE_2D, i.GLint, format, mipmapWidth, mipmapHeight, 0, 103 | pixelBytes, pImageData) 104 | else: 105 | glTexImage2D(GL_TEXTURE_2D, i.GLint, format.GLint, mipmapWidth, mipmapHeight, 106 | 0, format, typ, pImageData) 107 | offset += pixelBytes.uint 108 | inc i 109 | 110 | proc isPVRHeader*(data: openarray[byte]): bool = 111 | assert(data.len >= 4) 112 | let u = cast[ptr uint32](unsafeAddr data[0])[] 113 | u == 0x03525650 or u == 0x50565203 114 | -------------------------------------------------------------------------------- /nimx/private/js_platform_detector.nim: -------------------------------------------------------------------------------- 1 | when defined(js): 2 | proc isMacOsAux(): bool = 3 | {.emit: """ 4 | try { 5 | `result` = navigator.platform.indexOf("Mac") != -1; 6 | } catch(e) {} 7 | """.} 8 | let isMacOs* = isMacOsAux() 9 | elif defined(emscripten): 10 | import jsbind/emscripten 11 | proc isMacOsAux(): bool = 12 | let r = EM_ASM_INT(""" 13 | try { 14 | return navigator.platform.indexOf("Mac") != -1; 15 | } catch(e) {} 16 | """) 17 | result = cast[bool](r) 18 | let isMacOs* = isMacOsAux() 19 | -------------------------------------------------------------------------------- /nimx/private/kiwi_vector_symbolics.nim: -------------------------------------------------------------------------------- 1 | import kiwi 2 | 3 | proc equalConstraints(a, b: openarray[Expression], result: var openarray[Constraint]) = 4 | for i in 0 ..< a.len: 5 | result[i] = a[i] == b[i] 6 | 7 | proc ltConstraints(a, b: openarray[Expression], result: var openarray[Constraint]) = 8 | for i in 0 ..< a.len: 9 | result[i] = a[i] <= b[i] 10 | 11 | proc `==`*[I](a, b: array[I, Expression]): array[I, Constraint] {.inline.} = 12 | equalConstraints(a, b, result) 13 | 14 | proc `<=`*[I](a, b: array[I, Expression]): array[I, Constraint] {.inline.} = 15 | ltConstraints(a, b, result) 16 | 17 | template defineMath(op: untyped) = 18 | proc private(a: openarray[Expression], b: float32, result: var openarray[Expression]) = 19 | for i in 0 ..< a.len: 20 | result[i] = op(a[i], b) 21 | 22 | proc op*[I](a: array[I, Expression], b: float32): array[I, Expression] {.inline.} = 23 | private(a, b, result) 24 | 25 | defineMath(`+`) 26 | defineMath(`-`) 27 | -------------------------------------------------------------------------------- /nimx/private/objc_appkit.nim: -------------------------------------------------------------------------------- 1 | {.deprecated.} # Use darwin package instead 2 | {.passL: "-framework AppKit".} 3 | 4 | template enableObjC*() = 5 | ## Should be called in global scope of a nim file to ensure it will be 6 | ## translated to Objective-C 7 | block: 8 | proc dummyWithNoParticularMeaning() {.used, importobjc.} 9 | -------------------------------------------------------------------------------- /nimx/private/simple_table.nim: -------------------------------------------------------------------------------- 1 | # This is a table implementation that is more native to JS. It has a lot of 2 | # limitations as to key and value types. Please use with caution. 3 | 4 | when not defined(js): 5 | import tables 6 | export tables 7 | type SimpleTable*[TKey, TVal] = TableRef[TKey, TVal] 8 | template newSimpleTable*(TKey, TVal: typedesc): SimpleTable[TKey, TVal] = newTable[TKey, TVal]() 9 | else: 10 | type SimpleTable*[TKey, TVal] = ref object 11 | dummyk: TKey 12 | dummyv: TVal 13 | proc newSimpleTable*(TKey, TVal: typedesc): SimpleTable[TKey, TVal] {.importc: "new Object".} 14 | 15 | proc `[]`*[A, B](t: SimpleTable[A, B]; k: A): B {.importcpp: "#[#]".} 16 | proc `[]=`*[A, B](t: SimpleTable[A, B]; k: A, v: B) {.importcpp: "#[#]=#".} 17 | 18 | proc keyInTable[A, B](k: A, t: SimpleTable[A, B]): bool {.importcpp: "(# in #)".} 19 | template hasKey*[A, B](t: SimpleTable[A, B], k: A): bool = keyInTable(k, t) 20 | 21 | when isMainModule: 22 | let t = newSimpleTable(int, int) 23 | t[1] = 123 24 | doAssert(t[1] == 123) 25 | doAssert(t.hasKey(1)) 26 | -------------------------------------------------------------------------------- /nimx/private/worker_queue.nim: -------------------------------------------------------------------------------- 1 | import locks 2 | 3 | type TaskListNode = object 4 | p: proc(data: pointer) {.cdecl, gcsafe.} 5 | data: pointer 6 | next: ptr TaskListNode 7 | 8 | type 9 | WorkerQueue* = ref WorkerQueueObj 10 | WorkerQueueObj = object 11 | queueCond: Cond 12 | queueLock: Lock 13 | taskList: ptr TaskListNode 14 | lastTask: ptr TaskListNode 15 | threads: seq[Thread[pointer]] 16 | 17 | # proc finalize(w: WorkerQueue) = 18 | # # This will not work, so it's not used. 19 | # # This is a leak! It's used in nimx image because it's global anyway. 20 | # w.queueCond.deinitCond() 21 | # w.queueLock.deinitLock() 22 | 23 | proc threadWorker(qu: pointer) {.thread.} = 24 | let q = cast[ptr WorkerQueueObj](qu) 25 | while true: 26 | var t: ptr TaskListNode 27 | q.queueLock.acquire() 28 | 29 | while q.taskList.isNil: 30 | q.queueCond.wait(q.queueLock) 31 | 32 | t = q.taskList 33 | if q.lastTask == q.taskList: 34 | q.taskList = nil 35 | q.lastTask = nil 36 | else: 37 | q.taskList = t.next 38 | 39 | q.queueLock.release() 40 | t.p(t.data) 41 | deallocShared(t) 42 | 43 | proc newWorkerQueue*(maxThreads : int = 0): WorkerQueue = 44 | result.new() 45 | result.queueCond.initCond() 46 | result.queueLock.initLock() 47 | let mt = if maxThreads == 0: 2 else: maxThreads 48 | result.threads.newSeq(mt) 49 | for i in 0 ..< mt: 50 | result.threads[i].createThread(threadWorker, cast[pointer](result)) 51 | 52 | proc addTask*(q: WorkerQueue, p: proc(data: pointer) {.cdecl, gcsafe.}, data: pointer) = 53 | let task = cast[ptr TaskListNode](allocShared(sizeof(TaskListNode))) 54 | task.p = p 55 | task.data = data 56 | q.queueLock.acquire() 57 | if q.taskList.isNil: 58 | q.taskList = task 59 | q.lastTask = task 60 | else: 61 | q.lastTask.next = task 62 | q.lastTask = task 63 | q.queueCond.signal() 64 | q.queueLock.release() 65 | 66 | when isMainModule: 67 | import os 68 | proc worker(data: pointer) {.cdecl.} = 69 | echo "worker doing: ", cast[int](data) 70 | sleep(500) 71 | 72 | let q = newWorkerQueue(4) 73 | q.addTask(worker, cast[pointer](1)) 74 | q.addTask(worker, cast[pointer](2)) 75 | q.addTask(worker, cast[pointer](3)) 76 | q.addTask(worker, cast[pointer](4)) 77 | sleep(5000) 78 | -------------------------------------------------------------------------------- /nimx/progress_indicator.nim: -------------------------------------------------------------------------------- 1 | import view 2 | export view 3 | 4 | import composition 5 | import animation 6 | import abstract_window 7 | 8 | import times 9 | import math 10 | 11 | type ProgressIndicator* = ref object of View 12 | mValue: Coord 13 | mIndeterminate: bool 14 | animation: Animation 15 | 16 | const piComposition = newComposition """ 17 | uniform float uPosition; 18 | 19 | float radius = 5.0; 20 | 21 | void compose() { 22 | float stroke = sdRoundedRect(bounds, radius); 23 | float fill = sdRoundedRect(insetRect(bounds, 1.0), radius - 1.0); 24 | drawShape(stroke, newGrayColor(0.78)); 25 | drawShape(fill, newGrayColor(0.88)); 26 | 27 | vec4 progressRect = bounds; 28 | progressRect.z *= uPosition; 29 | vec4 fc = gradient(smoothstep(bounds.y, bounds.y + bounds.w, vPos.y), 30 | vec4(0.71, 0.80, 0.88, 1.0), 31 | 0.5, vec4(0.32, 0.68, 0.95, 1.0), 32 | vec4(0.71, 0.80, 0.88, 1.0)); 33 | 34 | drawShape(sdAnd(fill, sdRect(progressRect)), fc); 35 | } 36 | """ 37 | 38 | const indeterminateComposition = newComposition """ 39 | uniform float uPosition; 40 | 41 | float radius = 5.0; 42 | 43 | void compose() { 44 | float stroke = sdRoundedRect(bounds, radius); 45 | float fill = sdRoundedRect(insetRect(bounds, 1.0), radius - 1.0); 46 | drawShape(stroke, newGrayColor(0.78)); 47 | 48 | float st = 20.0; 49 | float p = mod(vPos.x + vPos.y - uPosition * st * 3.0, st); 50 | vec4 fc = gradient(smoothstep(0.0, st, p), 51 | vec4(0.88, 0.88, 0.88, 1.0), 52 | 0.5, vec4(0.32, 0.68, 0.95, 1.0), 53 | vec4(0.88, 0.88, 0.88, 1.0)); 54 | 55 | drawShape(fill, fc); 56 | 57 | vec4 glossColor = gradient(smoothstep(bounds.y, bounds.y + bounds.w / 2.0, vPos.y), 58 | vec4(1.0, 1.0, 1.0, 0.5), 59 | 0.75, vec4(1.0, 1.0, 1.0, 0.8), 60 | vec4(1.0, 1.0, 1.0, 1.0) 61 | ); 62 | glossColor.a *= 0.65; 63 | 64 | vec4 glossRect = bounds; 65 | glossRect.w /= 2.0; 66 | blendShape(sdAnd(fill, sdRect(glossRect)), glossColor); 67 | } 68 | """ 69 | 70 | method init(v: ProgressIndicator, r: Rect) = 71 | procCall v.View.init(r) 72 | v.animation = newAnimation() 73 | v.animation.finished = true 74 | v.animation.onAnimate = proc(p: float) = 75 | v.setNeedsDisplay() 76 | 77 | method draw*(v: ProgressIndicator, r: Rect) = 78 | if v.mIndeterminate: 79 | indeterminateComposition.draw v.bounds: 80 | setUniform("uPosition", float32(epochTime() mod 1.0)) 81 | else: 82 | piComposition.draw v.bounds: 83 | setUniform("uPosition", v.mValue) 84 | 85 | proc `value=`*(v: ProgressIndicator, p: Coord) = 86 | v.mValue = p 87 | v.setNeedsDisplay() 88 | 89 | proc value*(v: ProgressIndicator): Coord = v.mValue 90 | 91 | proc indeterminate*(v: ProgressIndicator): bool = v.mIndeterminate 92 | proc `indeterminate=`*(v: ProgressIndicator, flag: bool) = 93 | v.mIndeterminate = flag 94 | if v.animation.finished and flag and not v.window.isNil: 95 | v.window.addAnimation(v.animation) 96 | elif not flag: 97 | v.animation.cancel() 98 | 99 | method viewWillMoveToWindow*(v: ProgressIndicator, w: Window) = 100 | if w.isNil: 101 | v.animation.cancel() 102 | elif v.mIndeterminate: 103 | w.addAnimation(v.animation) 104 | -------------------------------------------------------------------------------- /nimx/property_editors/autoresizing_mask_editor.nim: -------------------------------------------------------------------------------- 1 | import nimx/property_visitor 2 | import nimx/numeric_text_field 3 | import nimx/popup_button 4 | import nimx/linear_layout 5 | 6 | import nimx/property_editors/propedit_registry 7 | 8 | import variant 9 | 10 | proc newAutoresizingMaskPropertyView(setter: proc(s: set[AutoresizingFlag]) {.gcsafe.}, getter: proc(): set[AutoresizingFlag] {.gcsafe.}): PropertyEditorView = 11 | result = PropertyEditorView.new(newRect(0, 0, 208, editorRowHeight)) 12 | 13 | let horLayout = newHorizontalLayout(newRect(0, 0, 208, editorRowHeight)) 14 | horLayout.autoresizingMask = {afFlexibleWidth, afFlexibleMaxY} 15 | result.addSubview(horLayout) 16 | 17 | var val = getter() 18 | let horEdit = PopupButton.new(newRect(0, 0, 40, editorRowHeight)) 19 | horEdit.items = @["flexible left", "flexible width", "flexible right"] 20 | horEdit.onAction do(): 21 | var newFlag = afFlexibleMaxX 22 | case horEdit.selectedIndex 23 | of 0: newFlag = afFlexibleMinX 24 | of 1: newFlag = afFlexibleWidth 25 | else: discard 26 | val = val - {afFlexibleMaxX, afFlexibleMinX, afFlexibleWidth} + {newFlag} 27 | setter(val) 28 | 29 | if afFlexibleMinX in val: 30 | horEdit.selectedIndex = 0 31 | elif afFlexibleWidth in val: 32 | horEdit.selectedIndex = 1 33 | else: 34 | horEdit.selectedIndex = 2 35 | 36 | horLayout.addSubview(horEdit) 37 | 38 | let vertEdit = PopupButton.new(newRect(40, 0, 40, editorRowHeight)) 39 | vertEdit.items = @["flexible top", "flexible height", "flexible bottom"] 40 | vertEdit.selectedIndex = 0 41 | vertEdit.onAction do(): 42 | var newFlag = afFlexibleMaxY 43 | case vertEdit.selectedIndex 44 | of 0: newFlag = afFlexibleMinY 45 | of 1: newFlag = afFlexibleHeight 46 | else: discard 47 | val = val - {afFlexibleMaxY, afFlexibleMinY, afFlexibleHeight} + {newFlag} 48 | setter(val) 49 | 50 | if afFlexibleMinY in val: 51 | vertEdit.selectedIndex = 0 52 | elif afFlexibleHeight in val: 53 | vertEdit.selectedIndex = 1 54 | else: 55 | vertEdit.selectedIndex = 2 56 | 57 | horLayout.addSubview(vertEdit) 58 | 59 | registerPropertyEditor(newAutoresizingMaskPropertyView) 60 | -------------------------------------------------------------------------------- /nimx/property_editors/propedit_registry.nim: -------------------------------------------------------------------------------- 1 | import tables 2 | import nimx/view 3 | import nimx/text_field 4 | import nimx/font 5 | import nimx/property_visitor 6 | 7 | import variant 8 | 9 | type 10 | PropertyEditorView* = ref object of View 11 | onChange*: proc() {.gcsafe.} 12 | changeInspector*: proc() {.gcsafe.} 13 | PropertyEditorCreatorWO*[T] = proc(editedObject: Variant, setter: proc(s: T) {.gcsafe.}, getter: proc(): T {.gcsafe.}): PropertyEditorView {.gcsafe.} 14 | PropertyEditorCreator*[T] = proc(setter: proc(s: T) {.gcsafe.}, getter: proc(): T {.gcsafe.}): PropertyEditorView {.gcsafe.} 15 | RegistryTableEntry = proc(editedObject: Variant, v: Variant): PropertyEditorView {.gcsafe.} 16 | 17 | var propEditors {.threadvar.}: Table[TypeId, RegistryTableEntry] 18 | propEditors = initTable[TypeId, RegistryTableEntry]() 19 | 20 | proc registerPropertyEditorAUX[T, C](createView: C) = 21 | propEditors[getTypeId(SetterAndGetter[T])] = proc(n: Variant, v: Variant): PropertyEditorView {.gcsafe.} = 22 | let sng = v.get(SetterAndGetter[T]) 23 | var r: PropertyEditorView 24 | proc setterAUX(s: T) {.gcsafe.} = 25 | sng.setter(s) 26 | if not r.isNil and not r.onChange.isNil: 27 | r.onChange() 28 | when C is PropertyEditorCreatorWO: 29 | r = createView(n, setterAUX, sng.getter) 30 | else: 31 | r = createView(setterAUX, sng.getter) 32 | result = r 33 | 34 | proc registerPropertyEditor*[T](createView: PropertyEditorCreatorWO[T]) = 35 | registerPropertyEditorAUX[T, PropertyEditorCreatorWO[T]](createView) 36 | 37 | proc registerPropertyEditor*[T](createView: PropertyEditorCreator[T]) = 38 | registerPropertyEditorAUX[T, PropertyEditorCreator[T]](createView) 39 | 40 | var gEditorFont {.threadvar.}: Font 41 | 42 | proc editorFont*(): Font {.gcsafe.} = 43 | if gEditorFont.isNil: gEditorFont = systemFontOfSize(14) 44 | result = gEditorFont 45 | 46 | const editorRowHeight* = 16 47 | 48 | template createEditorAUX(r: Rect) = 49 | let editor = creator(editedObject, v) 50 | editor.name = "editor" 51 | editor.setFrameOrigin(r.origin) 52 | var sz = newSize(r.size.width, editor.frame.height) 53 | editor.setFrameSize(sz) 54 | editor.autoresizingMask = {afFlexibleWidth, afFlexibleMaxY} 55 | result.addSubview(editor) 56 | 57 | sz = result.frame.size 58 | sz.height = editor.frame.height 59 | result.setFrameSize(sz) 60 | 61 | editor.changeInspector = changeInspectorCallback 62 | editor.onChange = onChange 63 | 64 | proc propertyEditorForProperty*(editedObject: Variant, title: string, v: Variant, onChange, changeInspectorCallback: proc() {.gcsafe.} = nil): View = 65 | let creator = propEditors.getOrDefault(v.typeId) 66 | result = View.new(newRect(0, 0, 328, editorRowHeight)) 67 | result.name = "'" & title & "'" 68 | result.autoresizingMask = {afFlexibleWidth, afFlexibleMaxY} 69 | let label = newLabel(newRect(0, 0, 100, editorRowHeight)) 70 | label.textColor = blackColor() 71 | label.name = "label" 72 | label.text = title & ":" 73 | label.font = editorFont() 74 | result.addSubview(label) 75 | if creator.isNil: 76 | label.text = title & " - Unknown property" 77 | else: 78 | createEditorAUX(newRect(label.frame.width, 0, result.bounds.width - label.frame.width, result.bounds.height)) 79 | 80 | proc propertyEditorForProperty*(editedObject: Variant, v: Variant, changeInspectorCallback: proc() {.gcsafe.} = nil): View = 81 | let creator = propEditors.getOrDefault(v.typeId) 82 | result = View.new(newRect(0, 0, 228, editorRowHeight)) 83 | result.autoresizingMask = {afFlexibleWidth, afFlexibleMaxY} 84 | if creator.isNil: 85 | discard result.newLabel(newPoint(100, 0), newSize(128, editorRowHeight), "Unknown") 86 | else: 87 | const onChange: proc() {.gcsafe.} = nil 88 | createEditorAUX(newRect(0,0, result.bounds.width, result.bounds.height)) -------------------------------------------------------------------------------- /nimx/property_visitor.nim: -------------------------------------------------------------------------------- 1 | import nimx/types 2 | 3 | import tables 4 | import variant 5 | 6 | type 7 | Setter*[T] = proc(v: T) {.gcsafe.} 8 | Getter*[T] = proc(): T {.gcsafe.} 9 | SetterAndGetter*[T] = tuple[setter: Setter[T], getter: Getter[T]] 10 | 11 | PropertyFlag* = enum 12 | pfEditable 13 | pfAnimatable 14 | 15 | EnumValue* = object 16 | possibleValues*: Table[string, int] 17 | curValue*: int 18 | 19 | PropertyVisitor* = object 20 | qualifiers: seq[string] 21 | requireSetter*: bool 22 | requireGetter*: bool 23 | requireName*: bool 24 | flags*: set[PropertyFlag] 25 | 26 | setterAndGetter*: Variant 27 | 28 | name*: string 29 | commit*: proc() {.gcsafe.} 30 | onChangeCallback* {.deprecated.}: proc() 31 | 32 | proc clear*(p: var PropertyVisitor) = 33 | p.setterAndGetter = newVariant() 34 | 35 | proc pushQualifier*(p: var PropertyVisitor, q: string) = 36 | p.qualifiers.add(q) 37 | 38 | proc popQualifier*(p: var PropertyVisitor) = 39 | p.qualifiers.setLen(p.qualifiers.len - 1) 40 | 41 | template visitProperty*(p: PropertyVisitor, propName: string, s: untyped, defFlags: set[PropertyFlag] = { pfEditable, pfAnimatable }) = 42 | if (defFlags * p.flags) != {}: 43 | when s is enum: 44 | var sng : SetterAndGetter[EnumValue] 45 | else: 46 | var sng : SetterAndGetter[type(s)] 47 | 48 | if p.requireSetter: 49 | when s is enum: 50 | sng.setter = proc(v: EnumValue) {.gcsafe.} = 51 | s = type(s)(v.curValue) 52 | else: 53 | sng.setter = proc(v: type(s)) {.gcsafe.} = s = v 54 | if p.requireGetter: 55 | when s is enum: 56 | sng.getter = proc(): EnumValue = 57 | result.possibleValues = initTable[string, int]() 58 | for i in low(type(s)) .. high(type(s)): 59 | result.possibleValues[$i] = ord(i) 60 | result.curValue = ord(s) 61 | else: 62 | sng.getter = proc(): type(s) = s 63 | if p.requireName: 64 | p.name = propName 65 | p.setterAndGetter = newVariant(sng) 66 | p.commit() 67 | 68 | template visitProperty*(p: PropertyVisitor, propName: string, s: untyped, onChange: proc() {.gcsafe.} ) {.deprecated.} = 69 | var defFlags = { pfEditable, pfAnimatable } 70 | if (defFlags * p.flags) != {}: 71 | when s is enum: 72 | var sng : SetterAndGetter[EnumValue] 73 | else: 74 | var sng : SetterAndGetter[type(s)] 75 | 76 | if p.requireSetter: 77 | when s is enum: 78 | sng.setter = proc(v: EnumValue) {.gcsafe.} = 79 | s = type(s)(v.curValue) 80 | else: 81 | sng.setter = proc(v: type(s)) {.gcsafe.} = s = v 82 | if p.requireGetter: 83 | when s is enum: 84 | sng.getter = proc(): EnumValue = 85 | result.possibleValues = initTable[string, int]() 86 | for i in low(type(s)) .. high(type(s)): 87 | result.possibleValues[$i] = ord(i) 88 | result.curValue = ord(s) 89 | else: 90 | sng.getter = proc(): type(s) = s 91 | if p.requireName: 92 | p.name = propName 93 | p.setterAndGetter = newVariant(sng) 94 | 95 | p.commit() 96 | -------------------------------------------------------------------------------- /nimx/rect_packer.nim: -------------------------------------------------------------------------------- 1 | {.deprecated.} # This has moved to a separate nimble package "rect_packer" 2 | type Rect = tuple[x, y, width, height: int32] 3 | 4 | type RectPacker* = ref object 5 | c1, c2: RectPacker 6 | rect: Rect 7 | occupied: bool 8 | maxX*, maxY*: int32 9 | 10 | proc newPacker*(width, height: int32): RectPacker = 11 | result.new() 12 | result.rect.width = width 13 | result.rect.height = height 14 | result.maxX = -1 15 | result.maxY = -1 16 | 17 | type Point = tuple[x, y: int32] 18 | 19 | template hasSpace*(a: Point): bool = a.x >= 0 20 | 21 | proc width*(p: RectPacker): int32 = p.rect.width 22 | proc height*(p: RectPacker): int32 = p.rect.height 23 | 24 | proc pack*(p: RectPacker, width, height: int32): Point = 25 | if not p.c1.isNil: 26 | # We are leaf 27 | result = p.c1.pack(width, height) 28 | if result.hasSpace: return 29 | result = p.c2.pack(width, height) 30 | else: 31 | # If we are occupied, return 32 | # If we are too small, return 33 | if p.occupied or width > p.rect.width or height > p.rect.height: 34 | return (-1.int32, -1.int32) 35 | 36 | # If we're just right, accept 37 | if width == p.rect.width and height == p.rect.height: 38 | p.occupied = true 39 | return (p.rect.x, p.rect.y) 40 | 41 | # Otherwise, gotta split this node and create some kids 42 | p.c1.new() 43 | p.c2.new() 44 | 45 | # decide which way to split 46 | let dw = p.rect.width - width 47 | let dh = p.rect.height - height 48 | 49 | if dw > dh: 50 | p.c1.rect = (p.rect.x, p.rect.y, width, p.rect.height) 51 | p.c2.rect = (p.rect.x + width, p.rect.y, p.rect.width - width, p.rect.height) 52 | else: 53 | p.c1.rect = (p.rect.x, p.rect.y, p.rect.width, height) 54 | p.c2.rect = (p.rect.x, p.rect.y + height, p.rect.width, p.rect.height - height) 55 | 56 | # insert into first child we created 57 | result = p.c1.pack(width, height) 58 | 59 | proc packAndGrow*(p: var RectPacker, width, height: int32): Point = 60 | while true: 61 | result = p.pack(width, height) 62 | if result.hasSpace: break 63 | var newP : RectPacker 64 | if p.width < p.height and (p.maxX == -1 or p.maxX >= p.width * 2): 65 | newP = newPacker(p.width * 2, p.height) 66 | newP.c2.new() 67 | newP.c2.rect.x = p.width 68 | newP.c2.rect.width = p.width 69 | newP.c2.rect.y = 0 70 | newP.c2.rect.height = p.height 71 | elif p.maxY == -1 or p.maxY >= p.height * 2: 72 | newP = newPacker(p.width, p.height * 2) 73 | newP.c2.new() 74 | newP.c2.rect.x = 0 75 | newP.c2.rect.width = p.width 76 | newP.c2.rect.y = p.height 77 | newP.c2.rect.height = p.height 78 | else: 79 | break 80 | newP.maxX = p.maxX 81 | newP.maxY = p.maxY 82 | newP.c1 = p 83 | p = newP 84 | -------------------------------------------------------------------------------- /nimx/screen.nim: -------------------------------------------------------------------------------- 1 | when defined(ios): 2 | import darwin/ui_kit 3 | elif defined(macosx): 4 | import darwin/app_kit 5 | elif defined(android): 6 | import android/util/display_metrics 7 | import android/app/activity 8 | import android/content/context_wrapper 9 | import android/content/res/resources 10 | 11 | elif defined(emscripten): 12 | import jsbind/emscripten 13 | 14 | proc screenScaleFactor*(): float = 15 | when defined(macosx) or defined(ios): 16 | result = mainScreen().scaleFactor() 17 | elif defined(js): 18 | asm "`result` = window.devicePixelRatio;" 19 | elif defined(android): 20 | let sm = currentActivity().getResources().getDisplayMetrics() 21 | result = sm.scaledDensity 22 | elif defined(emscripten): 23 | result = emscripten_get_device_pixel_ratio() 24 | else: 25 | result = 1.0 26 | -------------------------------------------------------------------------------- /nimx/scroll_bar.nim: -------------------------------------------------------------------------------- 1 | import slider 2 | export slider 3 | 4 | import composition, context, view_event_handling 5 | 6 | type ScrollBar* = ref object of Slider 7 | mKnobSize: float # Knob size should vary between 0.0 and 1.0 depending on 8 | # shown part of document in the clip view. E.g. if all of 9 | # the document fits, then it should be 1.0. Half of the 10 | # document is 0.5. 11 | trackingPos: Coord # Position of mouse coordinate (x or y depending on orientation) within knob 12 | 13 | const minKnobSize = 0.05 14 | method init*(s: ScrollBar, r: Rect) = 15 | procCall s.Slider.init(r) 16 | s.mKnobSize = 0.2 17 | 18 | proc knobRect(s: ScrollBar): Rect = 19 | result = s.bounds 20 | if s.isHorizontal: 21 | result.size.width *= max(s.mKnobSize, minKnobSize) 22 | result.origin.x = max((s.bounds.width - result.width) * s.value, 3) 23 | if result.maxX > s.bounds.width - 3: 24 | result.size.width = s.bounds.width - result.x - 3 25 | result = inset(result, 0, 2) 26 | else: 27 | result.size.height *= max(s.mKnobSize, minKnobSize) 28 | result.origin.y = max((s.bounds.height - result.height) * s.value, 3) 29 | if result.maxY > s.bounds.height - 3: 30 | result.size.height = s.bounds.height - result.y - 3 31 | result = inset(result, 2, 0) 32 | 33 | method draw*(s: ScrollBar, r: Rect) = 34 | let bezelRect = s.bounds.inset(1, 1) 35 | var radius = min(bezelRect.width, bezelRect.height) / 2 36 | let c = currentContext() 37 | c.fillColor = newGrayColor(0.85, 0.5) 38 | c.strokeColor = newGrayColor(0.5, 0.5) 39 | c.strokeWidth = 0.5 40 | c.drawRoundedRect(bezelRect, radius) 41 | 42 | let kr = s.knobRect() 43 | radius = min(kr.width, kr.height) / 2 44 | c.strokeWidth = 1 45 | c.fillColor = newColor(0.2, 0.2, 0.3, 0.5) 46 | c.strokeColor = newGrayColor(0.65, 0.5) 47 | c.drawRoundedRect(kr, radius) 48 | 49 | proc `knobSize=`*(s: ScrollBar, v: float) = 50 | s.mKnobSize = v 51 | if s.mKnobSize < 0: s.mKnobSize = 0 52 | elif s.mKnobSize > 1: s.mKnobSize = 1 53 | 54 | template knobSize*(s: ScrollBar): float = s.mKnobSize 55 | 56 | method onTouchEv*(s: ScrollBar, e: var Event): bool = 57 | template pageUp() = 58 | s.value = s.value - s.mKnobSize 59 | s.sendAction() 60 | 61 | template pageDown() = 62 | s.value = s.value + s.mKnobSize 63 | s.sendAction() 64 | 65 | let kr = s.knobRect() 66 | case e.buttonState 67 | of bsDown: 68 | if e.localPosition.inRect(kr): 69 | if s.isHorizontal: 70 | s.trackingPos = e.localPosition.x - kr.x 71 | else: 72 | s.trackingPos = e.localPosition.y - kr.y 73 | result = true 74 | else: 75 | if s.isHorizontal: 76 | if e.localPosition.x < kr.x: 77 | pageUp() 78 | else: 79 | pageDown() 80 | else: 81 | if e.localPosition.y < kr.y: 82 | pageUp() 83 | else: 84 | pageDown() 85 | of bsUnknown: 86 | if s.isHorizontal: 87 | let x = e.localPosition.x - s.trackingPos 88 | let ew = s.bounds.width - kr.width 89 | s.value = if ew > 0: x / ew else: 0 90 | else: 91 | let y = e.localPosition.y - s.trackingPos 92 | let eh = s.bounds.height - kr.height 93 | s.value = if eh > 0: y / eh else: 0 94 | s.sendAction() 95 | result = true 96 | of bsUp: 97 | discard 98 | -------------------------------------------------------------------------------- /nimx/slider.nim: -------------------------------------------------------------------------------- 1 | import control 2 | export control 3 | 4 | import composition 5 | import context 6 | import view_event_handling 7 | 8 | type Slider* = ref object of Control 9 | mValue: Coord 10 | 11 | const sliderComposition = newComposition """ 12 | uniform float uPosition; 13 | 14 | void compose() { 15 | float lineWidth = 4.0; 16 | float knobRadius = min(bounds.w, bounds.z) / 2.0 - 1.0; 17 | 18 | vec4 strokeColor = newGrayColor(0.78); 19 | 20 | if (bounds.z > bounds.w) { // Horizontal 21 | float knobPos = clamp(bounds.x + bounds.z * uPosition, knobRadius + 0.5, bounds.x + bounds.z - knobRadius - 0.5); 22 | 23 | float y = bounds.y + (bounds.w - lineWidth) / 2.0; 24 | 25 | vec4 firstPartRect = vec4(bounds.x, y, bounds.x + knobPos, lineWidth); 26 | vec4 secondPartRect = vec4(firstPartRect.z, y, bounds.x + bounds.z - knobPos, lineWidth); 27 | drawShape(sdRoundedRect(firstPartRect, lineWidth / 2.0), vec4(0.25, 0.60, 0.98, 1.0)); 28 | drawShape(sdRoundedRect(secondPartRect, lineWidth / 2.0), strokeColor); 29 | 30 | vec2 center = vec2(knobPos, bounds.y + bounds.w / 2.0); 31 | drawShape(sdCircle(center, knobRadius), strokeColor); 32 | drawShape(sdCircle(center, knobRadius - 1.0), newGrayColor(1.0)); 33 | } 34 | else { // Vertical 35 | float knobPos = clamp(bounds.y + bounds.w * uPosition, knobRadius + 0.5, bounds.y + bounds.w - knobRadius - 0.5); 36 | 37 | float x = bounds.x + (bounds.z - lineWidth) / 2.0; 38 | 39 | vec4 firstPartRect = vec4(x, bounds.y, lineWidth, bounds.y + knobPos); 40 | vec4 secondPartRect = vec4(x, firstPartRect.w, lineWidth, bounds.y + bounds.w - knobPos); 41 | drawShape(sdRoundedRect(firstPartRect, lineWidth / 2.0), vec4(0.25, 0.60, 0.98, 1.0)); 42 | drawShape(sdRoundedRect(secondPartRect, lineWidth / 2.0), strokeColor); 43 | 44 | vec2 center = vec2(bounds.x + bounds.z / 2.0, knobPos); 45 | drawShape(sdCircle(center, knobRadius), strokeColor); 46 | drawShape(sdCircle(center, knobRadius - 1.0), newGrayColor(1.0)); 47 | } 48 | } 49 | """ 50 | 51 | method draw*(s: Slider, r: Rect) = 52 | sliderComposition.draw s.bounds: 53 | setUniform("uPosition", s.mValue) 54 | 55 | proc `value=`*(s: Slider, p: Coord) = 56 | s.mValue = p 57 | if p < 0: s.mValue = 0 58 | elif p > 1: s.mValue = 1 59 | s.setNeedsDisplay() 60 | 61 | template value*(s: Slider): Coord = s.mValue 62 | 63 | template isHorizontal*(s: Slider): bool = s.bounds.width > s.bounds.height 64 | 65 | method onTouchEv(s: Slider, e: var Event): bool = 66 | result = true 67 | discard procCall s.View.onTouchEv(e) 68 | case e.buttonState 69 | of bsDown: 70 | if s.isHorizontal: 71 | s.value = e.localPosition.x / s.bounds.width 72 | else: 73 | s.value = e.localPosition.y / s.bounds.height 74 | of bsUnknown: 75 | if s.isHorizontal: 76 | s.value = e.localPosition.x / s.bounds.width 77 | else: 78 | s.value = e.localPosition.y / s.bounds.height 79 | s.setNeedsDisplay() 80 | s.sendAction(e) 81 | of bsUp: 82 | s.sendAction(e) 83 | result = false 84 | 85 | registerClass(Slider) 86 | -------------------------------------------------------------------------------- /nimx/stack_view.nim: -------------------------------------------------------------------------------- 1 | import nimx/[button, view, types, color] 2 | 3 | type StackView* = ref object of View 4 | 5 | method init*(v: StackView, r: Rect) = 6 | procCall v.View.init(r) 7 | v.backgroundColor = contentViewColor() 8 | 9 | proc recalculateContent(v: StackView) {.gcsafe.} 10 | proc newStackView*(r: Rect): StackView = 11 | result.new() 12 | result.init(r) 13 | result.recalculateContent() 14 | 15 | method draw(v: StackView, r: Rect) = 16 | procCall v.View.draw(r) 17 | 18 | proc recalculateContent(v: StackView) = 19 | var y = 0.0 20 | for sub in v.subviews: 21 | let frame = sub.frame 22 | sub.setFrameOrigin(newPoint(0, y)) 23 | 24 | y = y + frame.size.height 25 | 26 | var myFrame = v.frame 27 | myFrame.size.height = y 28 | v.setFrame(myFrame) 29 | 30 | method subviewDidChangeDesiredSize*(v: StackView, sub: View, desiredSize: Size) = 31 | v.recalculateContent() 32 | if not v.superview.isNil: 33 | v.superview.subviewDidChangeDesiredSize(v, v.frame().size) 34 | 35 | method didAddSubview*(v: StackView, subView: View) = 36 | v.recalculateContent() 37 | v.subviewDidChangeDesiredSize(nil, v.frame().size) 38 | 39 | method didRemoveSubview*(v: StackView, subView: View) = 40 | v.recalculateContent() 41 | v.subviewDidChangeDesiredSize(nil, v.frame().size) 42 | 43 | proc popupAtPoint*(ip: StackView, v: View, p: Point) = 44 | ip.removeFromSuperview() 45 | var origin: Point 46 | origin = v.convertPointToWindow(p) 47 | ip.setFrameOrigin(origin) 48 | v.window.addSubview(ip) 49 | -------------------------------------------------------------------------------- /nimx/system_logger.nim: -------------------------------------------------------------------------------- 1 | import strutils, logging 2 | 3 | when defined(js): 4 | proc native_log(a: cstring) {.importc: "console.log".} 5 | elif defined(emscripten): 6 | proc emscripten_log(flags: cint) {.importc, varargs.} 7 | template native_log(a: cstring) = 8 | emscripten_log(0, cstring("%s"), cstring(a)) 9 | elif defined(macosx) or defined(ios): 10 | {.passL:"-framework Foundation".} 11 | {.emit: """ 12 | 13 | #include 14 | extern void NSLog(CFStringRef format, ...); 15 | 16 | """.} 17 | 18 | proc native_log(a: cstring) = 19 | {.emit: "NSLog(CFSTR(\"%s\"), `a`);".} 20 | elif defined(android): 21 | proc log_write(prio: cint, tag, text: cstring) {.importc: "__android_log_write".} 22 | template native_log(a: cstring) = 23 | # ANDROID_LOG_INFO = 4 24 | log_write(4, "NIM_APP", a) 25 | else: 26 | template native_log(a: string) = echo a 27 | 28 | var currentOffset {.threadvar.}: string 29 | 30 | proc logi*(a: varargs[string, `$`]) {.gcsafe.} = 31 | native_log(currentOffset & a.join()) 32 | 33 | proc increaseOffset() = 34 | currentOffset &= " " 35 | 36 | template decreaseOffset() = 37 | currentOffset.setLen(currentOffset.len - 2) 38 | 39 | template enterLog*() = 40 | increaseOffset() 41 | defer: decreaseOffset() 42 | 43 | type SystemLogger = ref object of Logger 44 | 45 | method log*(logger: SystemLogger, level: Level, args: varargs[string, `$`]) = 46 | native_log(currentOffset & args.join()) 47 | 48 | proc registerLogger() = 49 | var lg: SystemLogger 50 | lg.new() 51 | addHandler(lg) 52 | 53 | registerLogger() 54 | -------------------------------------------------------------------------------- /nimx/table_view_cell.nim: -------------------------------------------------------------------------------- 1 | import view 2 | export view 3 | import context 4 | import types 5 | 6 | type TableViewCell* = ref object of View 7 | row*, col*: int 8 | selected*: bool 9 | 10 | proc newTableViewCell*(): TableViewCell = 11 | result.new() 12 | result.init(zeroRect) 13 | 14 | proc newTableViewCell*(r: Rect): TableViewCell = 15 | result.new() 16 | result.init(r) 17 | 18 | proc newTableViewCell*(s: Size): TableViewCell = 19 | newTableViewCell(newRect(zeroPoint, s)) 20 | 21 | proc newTableViewCell*(v: View): TableViewCell = 22 | result = newTableViewCell(v.frame.size) 23 | v.autoresizingMask = { afFlexibleWidth, afFlexibleHeight } 24 | result.addSubview(v) 25 | 26 | method selectionColor*(c: TableViewCell): Color {.base, gcsafe.} = 27 | return newColor(0.0, 0.0, 1.0) 28 | 29 | proc enclosingTableViewCell*(v: View): TableViewCell {.inline.} = 30 | v.enclosingViewOfType(TableViewCell) 31 | 32 | method draw*(c: TableViewCell, r: Rect) = 33 | if c.selected: 34 | let ctx = currentContext() 35 | ctx.fillColor = c.selectionColor() 36 | ctx.strokeWidth = 0 37 | ctx.drawRect(c.bounds) 38 | procCall c.View.draw(r) 39 | 40 | registerClass(TableViewCell) 41 | -------------------------------------------------------------------------------- /nimx/toolbar.nim: -------------------------------------------------------------------------------- 1 | {.used.} 2 | import nimx/context 3 | import nimx/view_dragging_listener 4 | import nimx/linear_layout 5 | 6 | type Toolbar* = ref object of LinearLayout 7 | 8 | method init*(v: Toolbar, r: Rect) = 9 | procCall v.LinearLayout.init(r) 10 | v.horizontal = true 11 | v.leftMargin = 10 12 | v.padding = 3 13 | v.topMargin = 3 14 | v.bottomMargin = 3 15 | v.rightMargin = 3 16 | v.enableDraggingByBackground() 17 | 18 | method draw*(view: Toolbar, rect: Rect) = 19 | let c = currentContext() 20 | c.strokeWidth = 2 21 | c.strokeColor = newGrayColor(0.6, 0.7) 22 | c.fillColor = newGrayColor(0.3, 0.7) 23 | c.drawRoundedRect(view.bounds, 5) 24 | -------------------------------------------------------------------------------- /nimx/undo_manager.nim: -------------------------------------------------------------------------------- 1 | 2 | 3 | type 4 | UndoManager* = ref object 5 | actions: seq[UndoAction] 6 | cursor: int 7 | 8 | UndoAction = object 9 | redo: proc() {.gcsafe.} 10 | undo: proc() {.gcsafe.} 11 | description: string 12 | 13 | var gUndoManager : UndoManager 14 | 15 | proc newUndoManager*(): UndoManager = 16 | result.new() 17 | result.actions = @[] 18 | 19 | proc sharedUndoManager*(): UndoManager = 20 | if gUndoManager.isNil: 21 | gUndoManager = newUndoManager() 22 | result = gUndoManager 23 | 24 | proc push*(u: UndoManager, description: string, redo: proc() {.gcsafe.}, undo: proc() {.gcsafe.}) {.inline.} = 25 | assert(not undo.isNil) 26 | if u.cursor != u.actions.len: 27 | u.actions.setLen(u.cursor) 28 | inc u.cursor 29 | u.actions.add(UndoAction(redo: redo, undo: undo, description: description)) 30 | 31 | proc pushAndDo*(u: UndoManager, description: string, redo: proc(){.gcsafe.}, undo: proc(){.gcsafe.}) {.inline.} = 32 | u.push(description, redo, undo) 33 | if not redo.isNil: redo() 34 | 35 | proc canUndo*(u: UndoManager): bool = u.cursor > 0 36 | proc canRedo*(u: UndoManager): bool = 37 | u.cursor >= 0 and 38 | u.cursor < u.actions.len and 39 | not u.actions[u.cursor].redo.isNil 40 | 41 | proc undo*(u: UndoManager) = 42 | assert(u.canUndo) 43 | dec u.cursor 44 | let action = u.actions[u.cursor] 45 | action.undo() 46 | 47 | proc redo*(u: UndoManager) = 48 | assert(u.canRedo) 49 | let action = u.actions[u.cursor] 50 | action.redo() 51 | inc u.cursor 52 | 53 | proc clear*(u: UndoManager) = 54 | u.actions.setLen(0) 55 | u.cursor = 0 56 | 57 | when isMainModule: 58 | let u = sharedUndoManager() 59 | u.pushAndDo("Move window") do(): 60 | echo "do1" 61 | do(): 62 | echo "undo1" 63 | 64 | u.undo() 65 | u.redo() 66 | 67 | u.pushAndDo("Move window2") do(): 68 | echo "do2" 69 | do(): 70 | echo "undo2" 71 | 72 | u.undo() 73 | u.undo() 74 | u.redo() 75 | u.redo() 76 | -------------------------------------------------------------------------------- /nimx/unistring.nim: -------------------------------------------------------------------------------- 1 | 2 | import unicode 3 | 4 | proc uniInsert*(dest: var string, src: string, position: int) = 5 | var r: Rune 6 | var charPos = 0 7 | var bytePos = 0 8 | while charPos < position: 9 | fastRuneAt(dest, bytePos, r, true) 10 | inc charPos 11 | dest.insert(src, bytePos) 12 | 13 | proc insert*(dest: var string, position: int, src: string) {.deprecated.} = 14 | dest.uniInsert(src, position) 15 | 16 | proc uniDelete*(subj: var string, start, stop: int) = 17 | var charPos = 0 18 | var byteStartPos = 0 19 | var r: Rune 20 | 21 | while charPos < start: 22 | fastRuneAt(subj, byteStartPos, r, true) 23 | inc charPos 24 | 25 | var byteEndPos = byteStartPos 26 | while charPos <= stop: 27 | fastRuneAt(subj, byteEndPos, r, true) 28 | inc charPos 29 | 30 | var bytesToCopy = byteEndPos - byteStartPos 31 | var i = 0 32 | while byteEndPos + i < subj.len: 33 | subj[byteStartPos + i] = subj[byteEndPos + i] 34 | inc i 35 | subj.setLen(subj.len - bytesToCopy) 36 | 37 | 38 | when isMainModule: 39 | proc testInsert(dest, src: string, pos: int, result: string) = 40 | var d = dest 41 | d.uniInsert(src, pos) 42 | assert(d == result) 43 | 44 | testInsert("123", "56", 0, "56123") 45 | testInsert("123", "56", 1, "15623") 46 | testInsert("123", "56", 3, "12356") 47 | testInsert("абвЙ", "є", 0, "єабвЙ") 48 | testInsert("абвЙ", "є", 2, "абєвЙ") 49 | testInsert("абвЙ", "є", 4, "абвЙє") 50 | 51 | proc testDelete(subj: string, start, stop: int, result: string) = 52 | var s = subj 53 | s.uniDelete(start, stop) 54 | assert(s == result) 55 | 56 | testDelete("hi", 0, 0, "i") 57 | testDelete("bi", 0, 1, "") 58 | testDelete("bye", 1, 1, "be") 59 | testDelete("bye", 2, 2, "by") 60 | testDelete("bye", 1, 2, "b") 61 | testDelete("asdf", 1, 1, "adf") 62 | testDelete("абвЙ", 1, 2, "аЙ") 63 | 64 | 65 | -------------------------------------------------------------------------------- /nimx/utils/android.nim: -------------------------------------------------------------------------------- 1 | when not defined(android): 2 | {.error: "This module is available only for Android target".} 3 | 4 | {.deprecated.} # Use android.app.activity module instead 5 | 6 | import android/app/activity 7 | 8 | proc mainActivity*(): Activity {.deprecated.} = 9 | # Use android.app.activity.currentActivity() 10 | currentActivity() 11 | -------------------------------------------------------------------------------- /nimx/utils/lower_bound.nim: -------------------------------------------------------------------------------- 1 | 2 | template lowerBoundIt*[T](arr: openarray[T], a, b: int, predicate: untyped): int = 3 | block: 4 | var res = a 5 | var count = b - res + 1 6 | var step, pos: int 7 | while count != 0: 8 | step = count div 2 9 | pos = res + step 10 | template it: T {.inject.} = arr[pos] 11 | if predicate: 12 | res = pos + 1 13 | count -= step + 1 14 | else: 15 | count = step 16 | res 17 | -------------------------------------------------------------------------------- /nimx/view_dragging_listener.nim: -------------------------------------------------------------------------------- 1 | import nimx/gesture_detector 2 | import nimx/view 3 | import nimx/event 4 | import nimx/context 5 | 6 | type DraggingScrollListener = ref object of OnScrollListener 7 | view: View 8 | start: Point 9 | 10 | method onTapDown(ls: DraggingScrollListener, e: var Event) = 11 | ls.start = ls.view.frame.origin 12 | 13 | method onScrollProgress(ls: DraggingScrollListener, dx, dy : float32, e : var Event) = 14 | ls.view.setFrameOrigin(ls.start + newPoint(dx, dy)) 15 | 16 | proc enableDraggingByBackground*(v: View) = 17 | var listener: DraggingScrollListener 18 | listener.new 19 | listener.view = v 20 | v.addGestureDetector(newScrollGestureDetector(listener)) 21 | 22 | type ResizingKnob = ref object of View 23 | 24 | type ResizingScrollListener = ref object of OnScrollListener 25 | view: View 26 | originalSize: Size 27 | 28 | method onTapDown(ls: ResizingScrollListener, e: var Event) = 29 | ls.originalSize = ls.view.superview.frame.size 30 | 31 | method onScrollProgress(ls: ResizingScrollListener, dx, dy : float32, e : var Event) = 32 | let v = ls.view.superview 33 | v.setFrameSize(ls.originalSize + newSize(dx, dy)) 34 | 35 | method draw(k: ResizingKnob, r: Rect) = 36 | let c = currentContext() 37 | c.strokeWidth = 2 38 | c.strokeColor = newGrayColor(0.2, 0.7) 39 | let b = k.bounds 40 | template drawAtOffset(o: Coord) = 41 | c.drawLine(newPoint(b.width * o, b.height), newPoint(b.width, b.height * o)) 42 | drawAtOffset(0) 43 | drawAtOffset(0.4) 44 | drawAtOffset(0.8) 45 | 46 | proc enableViewResizing*(v: View) = 47 | const size = 20 48 | let resizingKnob = ResizingKnob.new(newRect(v.bounds.width - size, v.bounds.height - size, size, size)) 49 | resizingKnob.autoresizingMask = {afFlexibleMinX, afFlexibleMinY} 50 | v.addSubview(resizingKnob) 51 | var listener: ResizingScrollListener 52 | listener.new() 53 | listener.view = resizingKnob 54 | resizingKnob.addGestureDetector(newScrollGestureDetector(listener)) 55 | -------------------------------------------------------------------------------- /nimx/view_event_handling_new.nim: -------------------------------------------------------------------------------- 1 | {.deprecated.} # Please use view_event_handling. Sorry about that. 2 | 3 | import view_event_handling 4 | export view_event_handling 5 | -------------------------------------------------------------------------------- /nimx/view_render_to_image.nim: -------------------------------------------------------------------------------- 1 | import nimx / [ view, render_to_image, image, types, context ] 2 | 3 | proc renderToImage*(v: View, image: SelfContainedImage)= 4 | image.draw: 5 | v.recursiveDrawSubviews() 6 | 7 | proc screenShot*(v: View):SelfContainedImage= 8 | var image = imageWithSize(v.bounds.size) 9 | v.renderToImage(image) 10 | result = image 11 | -------------------------------------------------------------------------------- /nimx/window.nim: -------------------------------------------------------------------------------- 1 | # Platform specific implementations follow 2 | import abstract_window 3 | when defined(js): 4 | import private/windows/js_canvas_window 5 | export startAnimation 6 | elif defined(emscripten): 7 | import private/windows/emscripten_window 8 | elif defined(wasm): 9 | import private/windows/web_window 10 | elif defined(macosx) and not defined(ios) and defined(nimxAvoidSDL): 11 | import private/windows/appkit_window 12 | elif defined(linux) and not defined(android) and defined(nimxAvoidSDL): 13 | import private/windows/x11_window 14 | elif defined(windows) and defined(nimxAvoidSDL): 15 | import private/windows/winapi_window 16 | else: 17 | import private/windows/sdl_window 18 | export runUntilQuit 19 | 20 | export runApplication 21 | 22 | export abstract_window 23 | -------------------------------------------------------------------------------- /nimx/window_event_handling.nim: -------------------------------------------------------------------------------- 1 | import types 2 | import abstract_window 3 | import view_event_handling 4 | import drag_and_drop 5 | 6 | proc propagateEventThroughResponderChain(w: Window, e: var Event): bool = 7 | var r = w.firstResponder 8 | while not result and not r.isNil and r != w: 9 | result = r.processKeyboardEvent(e) 10 | r = r.superview 11 | if not result: 12 | result = w.processKeyboardEvent(e) 13 | 14 | proc getOtherResponders(v: View, exceptV: View, responders: var seq[View]) = 15 | for sv in v.subviews: 16 | if sv != exceptV: 17 | if sv.acceptsFirstResponder(): 18 | responders.add sv 19 | sv.getOtherResponders(exceptV, responders) 20 | 21 | proc findNearestNextResponder(fromX: float, fromY: float, responders: seq[View], forward: bool): View = 22 | let sign: float = if forward: 1 else: -1 23 | var bestDH: float = Inf 24 | var bestDV: float = Inf 25 | var bestResponder: View 26 | for responder in responders: 27 | let responderRect = responder.convertRectToWindow(responder.bounds) 28 | var dH = (responderRect.minX - fromX) * sign 29 | var dV = (responderRect.minY - fromY) * sign 30 | if dV > 0 or (dV == 0 and dH > 0): 31 | if dV < bestDV or (dV == bestDV and dH < bestDH): 32 | bestResponder = responder 33 | bestDH = dH 34 | bestDV = dV 35 | return bestResponder 36 | 37 | method onKeyDown*(w: Window, e: var Event): bool = 38 | if e.keyCode == VirtualKey.Tab: 39 | let forward = not e.modifiers.anyShift() 40 | var curResp = w.firstResponder 41 | let firstRespRect = w.firstResponder.convertRectToWindow(w.firstResponder.bounds) 42 | var nextResponder: View 43 | 44 | while nextResponder.isNil and curResp != w: 45 | var responders: seq[View] = @[] 46 | getOtherResponders(curResp.superview, curResp, responders) 47 | if responders.len > 0: 48 | nextResponder = findNearestNextResponder(firstRespRect.minX, firstRespRect.minY, responders, forward) 49 | curResp = curResp.superview 50 | 51 | if nextResponder.isNil: 52 | var responders: seq[View] = @[] 53 | getOtherResponders(w, w.firstResponder, responders) 54 | if forward: 55 | nextResponder = findNearestNextResponder(w.bounds.minX, w.bounds.minY, responders, forward) 56 | else: 57 | nextResponder = findNearestNextResponder(w.bounds.maxX, w.bounds.maxY, responders, forward) 58 | 59 | if not nextResponder.isNil(): 60 | discard w.makeFirstResponder(nextResponder) 61 | 62 | return true 63 | 64 | method handleEvent*(w: Window, e: var Event): bool {.base, gcsafe.} = 65 | case e.kind: 66 | of etScroll: 67 | result = w.processMouseWheelEvent(e) 68 | of etMouse, etTouch: 69 | currentDragSystem().processDragEvent(e) 70 | w.handleMouseOverEvent(e) 71 | result = w.processTouchEvent(e) 72 | of etKeyboard: 73 | result = w.propagateEventThroughResponderChain(e) 74 | of etTextInput: 75 | result = w.propagateEventThroughResponderChain(e) 76 | of etWindowResized: 77 | result = true 78 | w.onResize(newSize(e.position.x, e.position.y)) 79 | w.drawWindow() 80 | else: 81 | result = false 82 | -------------------------------------------------------------------------------- /test/main.nim: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/nim c -r --threads:on 2 | import sample_registry 3 | 4 | import nimx / [ view, scroll_view, table_view, text_field, autotest, window, linear_layout ] 5 | import sequtils, intsets 6 | 7 | {.warning[UnusedImport]: off.} 8 | 9 | import sample01_welcome 10 | import sample02_controls 11 | import sample03_image 12 | import sample04_animation 13 | # import sample15_animation_easings 14 | import sample05_fonts 15 | import sample06_timers 16 | import sample07_collections 17 | import sample08_events 18 | import sample09_docking_tabs 19 | import sample10_text 20 | import sample11_expanded_views 21 | import sample12_menus 22 | import sample13_drag_and_drop 23 | import sample14_layout 24 | import sample16_outline 25 | 26 | 27 | const isMobile = defined(ios) or defined(android) 28 | 29 | proc startApplication() = 30 | let mainWindow = when isMobile: 31 | newFullscreenWindow() 32 | else: 33 | newWindow(newRect(40, 40, 800, 600)) 34 | 35 | mainWindow.title = "NimX Sample" 36 | 37 | var currentView = View.new(newRect(0, 0, mainWindow.bounds.width - 100, mainWindow.bounds.height)) 38 | 39 | let splitView = newHorizontalLayout(mainWindow.bounds) 40 | splitView.resizingMask = "wh" 41 | splitView.userResizeable = true 42 | mainWindow.addSubview(splitView) 43 | 44 | let tableView = newTableView(newRect(0, 0, 120, mainWindow.bounds.height)) 45 | tableView.resizingMask = "rh" 46 | splitView.addSubview(newScrollView(tableView)) 47 | splitView.addSubview(currentView) 48 | splitView.setDividerPosition(120, 0) 49 | 50 | tableView.numberOfRows = proc: int = allSamples.len 51 | tableView.createCell = proc (): TableViewCell = 52 | result = newTableViewCell(newLabel(newRect(0, 0, 120, 20))) 53 | tableView.configureCell = proc (c: TableViewCell) = 54 | TextField(c.subviews[0]).text = allSamples[c.row].name 55 | tableView.onSelectionChange = proc() = 56 | let selectedRows = toSeq(items(tableView.selectedRows)) 57 | if selectedRows.len > 0: 58 | let firstSelectedRow = selectedRows[0] 59 | let nv = View(newObjectOfClass(allSamples[firstSelectedRow].className)) 60 | nv.init(currentView.frame) 61 | nv.resizingMask = "wh" 62 | splitView.replaceSubview(currentView, nv) 63 | currentView = nv 64 | 65 | tableView.reloadData() 66 | tableView.selectRow(0) 67 | 68 | uiTest generalUITest: 69 | sendMouseDownEvent(mainWindow, newPoint(50, 60)) 70 | sendMouseUpEvent(mainWindow, newPoint(50, 60)) 71 | 72 | sendMouseDownEvent(mainWindow, newPoint(50, 90)) 73 | sendMouseUpEvent(mainWindow, newPoint(50, 90)) 74 | 75 | sendMouseDownEvent(mainWindow, newPoint(50, 120)) 76 | sendMouseUpEvent(mainWindow, newPoint(50, 120)) 77 | 78 | sendMouseDownEvent(mainWindow, newPoint(50, 90)) 79 | sendMouseUpEvent(mainWindow, newPoint(50, 90)) 80 | 81 | sendMouseDownEvent(mainWindow, newPoint(50, 60)) 82 | sendMouseUpEvent(mainWindow, newPoint(50, 60)) 83 | 84 | sendMouseDownEvent(mainWindow, newPoint(50, 30)) 85 | sendMouseUpEvent(mainWindow, newPoint(50, 30)) 86 | 87 | quitApplication() 88 | 89 | registerTest(generalUITest) 90 | when defined(runAutoTests): 91 | startRegisteredTests() 92 | 93 | runApplication: 94 | startApplication() 95 | -------------------------------------------------------------------------------- /test/nakefile.nim: -------------------------------------------------------------------------------- 1 | include "../nimx/naketools" 2 | 3 | beforeBuild = proc(b: Builder) = 4 | b.disableClosureCompiler = true 5 | if b.platform == "emscripten": 6 | b.additionalCompilerFlags.add("-g4") 7 | 8 | task "em", "Emscripten": 9 | let b = newBuilder("emscripten") 10 | b.build() 11 | -------------------------------------------------------------------------------- /test/res/Base.lproj/LaunchScreen.nib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/nimx/d93a6795a7e473627e52763deee0cb2df2d1a7fc/test/res/Base.lproj/LaunchScreen.nib -------------------------------------------------------------------------------- /test/res/Info.plist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/nimx/d93a6795a7e473627e52763deee0cb2df2d1a7fc/test/res/Info.plist -------------------------------------------------------------------------------- /test/res/MyGame.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/nimx/d93a6795a7e473627e52763deee0cb2df2d1a7fc/test/res/MyGame.ico -------------------------------------------------------------------------------- /test/res/OpenSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/nimx/d93a6795a7e473627e52763deee0cb2df2d1a7fc/test/res/OpenSans-Regular.ttf -------------------------------------------------------------------------------- /test/res/PkgInfo: -------------------------------------------------------------------------------- 1 | APPL???? -------------------------------------------------------------------------------- /test/res/cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/nimx/d93a6795a7e473627e52763deee0cb2df2d1a7fc/test/res/cat.jpg -------------------------------------------------------------------------------- /test/res/tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yglukhov/nimx/d93a6795a7e473627e52763deee0cb2df2d1a7fc/test/res/tile.png -------------------------------------------------------------------------------- /test/sample01_welcome.nim: -------------------------------------------------------------------------------- 1 | import sample_registry 2 | 3 | import nimx / [ view, font, context, composition, button, autotest, 4 | gesture_detector, view_event_handling ] 5 | 6 | const welcomeMessage = "Welcome to nimX" 7 | 8 | type WelcomeView = ref object of View 9 | welcomeFont: Font 10 | 11 | type CustomControl* = ref object of Control 12 | 13 | method onScroll*(v: CustomControl, e: var Event): bool = 14 | echo "custom scroll ", e.offset 15 | result = true 16 | 17 | method init(v: WelcomeView, r: Rect) = 18 | procCall v.View.init(r) 19 | let autoTestButton = newButton(newRect(20, 20, 150, 20)) 20 | let secondTestButton = newButton(newRect(20, 50, 150, 20)) 21 | autoTestButton.title = "Start Auto Tests" 22 | secondTestButton.title = "Second button" 23 | let tapd = newTapGestureDetector do(tapPoint : Point): 24 | echo "tap on second button" 25 | discard 26 | secondTestButton.addGestureDetector(tapd) 27 | autoTestButton.onAction do(): 28 | startRegisteredTests() 29 | secondTestButton.onAction do(): 30 | echo "second click" 31 | v.addSubview(autoTestButton) 32 | v.addSubview(secondTestButton) 33 | let vtapd = newTapGestureDetector do(tapPoint : Point): 34 | echo "tap on welcome view" 35 | discard 36 | v.addGestureDetector(vtapd) 37 | var cc: CustomControl 38 | cc.new 39 | cc.init(newRect(20, 80, 150, 20)) 40 | cc.clickable = true 41 | cc.backgroundColor = newColor(1.0,0.0,0.0,1.0) 42 | cc.onAction do(): 43 | echo "custom control clicked" 44 | let lis = newBaseScrollListener do(e : var Event): 45 | echo "tap down at: ",e.position 46 | do(dx, dy : float32, e : var Event): 47 | echo "scroll: ",e.position 48 | do(dx, dy : float32, e : var Event): 49 | echo "scroll end at: ",e.position 50 | let flingLis = newBaseFlingListener do(vx, vy: float): 51 | echo "flinged with velo: ",vx, " ",vy 52 | cc.addGestureDetector(newScrollGestureDetector(lis)) 53 | cc.addGestureDetector(newFlingGestureDetector(flingLis)) 54 | v.addSubview(cc) 55 | cc.trackMouseOver(true) 56 | 57 | const gradientComposition = newComposition """ 58 | void compose() { 59 | vec4 color = gradient(smoothstep(bounds.x, bounds.x + bounds.z, vPos.x), 60 | newGrayColor(0.7), 61 | 0.3, newGrayColor(0.5), 62 | 0.5, newGrayColor(0.7), 63 | 0.7, newGrayColor(0.5), 64 | newGrayColor(0.7) 65 | ); 66 | drawShape(sdRect(bounds), color); 67 | } 68 | """ 69 | 70 | method draw(v: WelcomeView, r: Rect) = 71 | let c = currentContext() 72 | if v.welcomeFont.isNil: 73 | v.welcomeFont = systemFontOfSize(64) 74 | gradientComposition.draw(v.bounds) 75 | let s = v.welcomeFont.sizeOfString(welcomeMessage) 76 | c.fillColor = whiteColor() 77 | c.drawText(v.welcomeFont, s.centerInRect(v.bounds), welcomeMessage) 78 | 79 | registerSample(WelcomeView, "Welcome") 80 | -------------------------------------------------------------------------------- /test/sample02_controls.nim: -------------------------------------------------------------------------------- 1 | import sample_registry 2 | 3 | import nimx / [ view, segmented_control, color_picker, button, image, image_view, 4 | text_field, slider, popup_button, progress_indicator ] 5 | import nimx/assets/asset_manager 6 | 7 | type ControlsSampleView = ref object of View 8 | 9 | method init(v: ControlsSampleView, r: Rect) = 10 | procCall v.View.init(r) 11 | 12 | let label = newLabel(newRect(10, 10, 100, 20)) 13 | let textField = newTextField(newRect(120, 10, v.bounds.width - 130, 20)) 14 | textField.autoresizingMask = { afFlexibleWidth, afFlexibleMaxY } 15 | label.text = "Text field:" 16 | v.addSubview(label) 17 | v.addSubview(textField) 18 | 19 | let button = newButton(newRect(10, 40, 100, 22)) 20 | button.title = "Button" 21 | button.onAction do(): 22 | textField.text = "Click! " 23 | v.addSubview(button) 24 | 25 | let sc = SegmentedControl.new(newRect(120, 40, v.bounds.width - 130, 22)) 26 | sc.segments = @["This", "is", "a", "segmented", "control"] 27 | sc.autoresizingMask = { afFlexibleWidth, afFlexibleMaxY } 28 | sc.onAction do(): 29 | textField.text = "Seg " & $sc.selectedSegment & "! " 30 | 31 | v.addSubview(sc) 32 | 33 | let checkbox = newCheckbox(newRect(10, 70, 50, 16)) 34 | checkbox.title = "Checkbox" 35 | v.addSubview(checkbox) 36 | 37 | let progress = ProgressIndicator.new(newRect(120, 130, v.bounds.width - 130, 16)) 38 | progress.autoresizingMask = { afFlexibleWidth, afFlexibleMaxY } 39 | v.addSubview(progress) 40 | 41 | let slider = Slider.new(newRect(120, 70, v.bounds.width - 130, 16)) 42 | slider.autoresizingMask = { afFlexibleWidth, afFlexibleMaxY } 43 | slider.onAction do(): 44 | textField.text = "Slider value: " & $slider.value & " " 45 | progress.value = slider.value 46 | v.addSubview(slider) 47 | 48 | let vertSlider = Slider.new(newRect(v.bounds.width - 26, 150, 16, v.bounds.height - 160)) 49 | vertSlider.autoresizingMask = { afFlexibleMinX, afFlexibleHeight } 50 | v.addSubview(vertSlider) 51 | 52 | let radiobox = newRadiobox(newRect(10, 90, 50, 16)) 53 | radiobox.title = "Radiobox" 54 | v.addSubview(radiobox) 55 | 56 | let indeterminateCheckbox = newCheckbox(newRect(10, 130, 100, 16)) 57 | indeterminateCheckbox.title = "Indeterminate" 58 | indeterminateCheckbox.onAction do(): 59 | progress.indeterminate = indeterminateCheckbox.boolValue 60 | v.addSubview(indeterminateCheckbox) 61 | 62 | let pb = PopupButton.new(newRect(120, 90, 120, 20)) 63 | pb.items = @["Popup button", "Item 1", "Item 2"] 64 | v.addSubview(pb) 65 | 66 | sharedAssetManager().getAssetAtPath("cat.jpg") do(i: Image, err: string): 67 | discard newImageButton(v, newPoint(260, 90), newSize(32, 32), i) 68 | 69 | let tfLabel = newLabel(newRect(330, 150, 150, 20)) 70 | tfLabel.text = "<-- Enter some text" 71 | let tf1 = newTextField(newRect(10, 150, 150, 20)) 72 | let tf2 = newTextField(newRect(170, 150, 150, 20)) 73 | tf1.onAction do(): 74 | tfLabel.text = "Left textfield: " & tf1.text 75 | tf2.onAction do(): 76 | tfLabel.text = "Right textfield: " & tf2.text 77 | 78 | v.addSubview(tfLabel) 79 | v.addSubview(tf1) 80 | v.addSubview(tf2) 81 | 82 | let cp = newColorPickerView(newRect(0, 0, 400, 170)) 83 | cp.setFrameOrigin(newPoint(10, 200)) 84 | cp.onColorSelected = proc(c: Color) = 85 | discard 86 | v.addSubview(cp) 87 | 88 | sharedAssetManager().getAssetAtPath("tile.png") do(i: Image, err: string): 89 | let imageView = newImageView(newRect(0, 400, 300, 150), i) 90 | v.addSubview(imageView) 91 | 92 | let popupFillRule = newPopupButton(v, newPoint(420, 400), newSize(100, 20), ["NoFill", "Stretch", "Tile", "FitWidth", "FitHeight"]) 93 | popupFillRule.onAction do(): 94 | imageView.fillRule = popupFillRule.selectedIndex().ImageFillRule 95 | 96 | registerSample(ControlsSampleView, "Controls") 97 | -------------------------------------------------------------------------------- /test/sample03_image.nim: -------------------------------------------------------------------------------- 1 | import sample_registry 2 | import nimx / [ view, image, context, render_to_image, font ] 3 | import nimx/assets/asset_manager 4 | 5 | type ImageSampleView = ref object of View 6 | image: Image 7 | generatedImage: Image 8 | httpImage: Image 9 | 10 | method init*(v: ImageSampleView, r: Rect) = 11 | procCall v.View.init(r) 12 | loadImageFromURL("http://gravatar.com/avatar/71b7b08fbc2f989a8246913ac608cca9") do(i: Image): 13 | v.httpImage = i 14 | v.setNeedsDisplay() 15 | 16 | sharedAssetManager().getAssetAtPath("cat.jpg") do(i: Image, err: string): 17 | v.image = i 18 | v.setNeedsDisplay() 19 | 20 | proc renderToImage(): Image = 21 | let r = imageWithSize(newSize(200, 80)) 22 | r.draw: 23 | let c = currentContext() 24 | c.fillColor = newColor(0.5, 0.5, 1) 25 | c.strokeColor = newColor(1, 0, 0) 26 | c.strokeWidth = 3 27 | c.drawRoundedRect(newRect(0, 0, 200, 80), 20) 28 | c.fillColor = blackColor() 29 | let font = systemFontOfSize(32) 30 | c.drawText(font, newPoint(10, 25), "Runtime image") 31 | result = r 32 | 33 | method draw(v: ImageSampleView, r: Rect) = 34 | if v.generatedImage.isNil: 35 | v.generatedImage = renderToImage() 36 | 37 | let c = currentContext() 38 | 39 | # Draw cat 40 | var imageSize = zeroSize 41 | if not v.image.isNil: 42 | imageSize = v.image.size 43 | var imageRect = newRect(zeroPoint, imageSize) 44 | imageRect.origin = imageSize.centerInRect(v.bounds) 45 | 46 | if not v.image.isNil: 47 | c.drawImage(v.image, imageRect) 48 | 49 | # Draw generatedImage 50 | imageRect.origin.x += imageSize.width - 60 51 | imageRect.origin.y += imageSize.height - 60 52 | imageRect.size = v.generatedImage.size 53 | c.drawImage(v.generatedImage, imageRect) 54 | 55 | if not v.httpImage.isNil: 56 | c.drawImage(v.httpImage, newRect(50, 50, 100, 100)) 57 | 58 | registerSample(ImageSampleView, "Image") 59 | -------------------------------------------------------------------------------- /test/sample04_animation.nim: -------------------------------------------------------------------------------- 1 | import math # for PI 2 | import sample_registry 3 | import nimx / [ view, context, animation, window, button, progress_indicator ] 4 | 5 | type AnimationSampleView = ref object of View 6 | rotation: Coord 7 | animation: Animation 8 | 9 | method init*(v: AnimationSampleView, r: Rect) = 10 | procCall v.View.init(r) 11 | v.animation = newAnimation() 12 | 13 | # Start/Stop button 14 | let startStopButton = newButton(newRect(20, 20, 50, 50)) 15 | startStopButton.title = "Stop" 16 | startStopButton.onAction do(): 17 | if v.animation.finished: 18 | v.window.addAnimation(v.animation) 19 | startStopButton.title = "Stop" 20 | else: 21 | v.animation.cancel() 22 | v.addSubview(startStopButton) 23 | 24 | v.animation.timingFunction = bezierTimingFunction(0.53,-0.53,0.38,1.52) 25 | v.animation.onAnimate = proc(p: float) = 26 | v.rotation = p * PI * 2 27 | v.animation.loopDuration = 2.0 28 | v.animation.onComplete do(): 29 | startStopButton.title = "Start" 30 | #v.animation.numberOfLoops = 2 31 | 32 | let playPauseButton = newButton(newRect(80, 20, 70, 50)) 33 | playPauseButton.title = "Pause" 34 | playPauseButton.onAction do(): 35 | if playPauseButton.title == "Pause": 36 | v.animation.pause() 37 | playPauseButton.title = "Resume" 38 | else: 39 | v.animation.resume() 40 | playPauseButton.title = "Pause" 41 | v.addSubview(playPauseButton) 42 | 43 | 44 | let progressBar = ProgressIndicator.new(newRect(160, 20, 90, 20)) 45 | v.addSubview(progressBar) 46 | 47 | # Loop progress handlers are called when animation reaches specified loop progress. 48 | v.animation.addLoopProgressHandler 1.0, false, proc() = 49 | progressBar.value = 1.0 50 | 51 | v.animation.addLoopProgressHandler 0.5, false, proc() = 52 | progressBar.value = 0.5 53 | 54 | v.animation.continueUntilEndOfLoopOnCancel = true 55 | 56 | method draw(v: AnimationSampleView, r: Rect) = 57 | let c = currentContext() 58 | c.fillColor = newGrayColor(0.5) 59 | var tmpTransform = c.transform 60 | tmpTransform.translate(newVector3(v.bounds.width/2, v.bounds.height/3, 0)) 61 | tmpTransform.rotateZ(v.rotation) 62 | tmpTransform.translate(newVector3(-50, -50, 0)) 63 | c.withTransform tmpTransform: 64 | c.fillColor = newColor(0, 1, 1) 65 | c.strokeColor = newColor(0, 0, 0, 1) 66 | c.strokeWidth = 9.0 67 | c.drawEllipseInRect(newRect(0, 0, 100, 200)) 68 | 69 | tmpTransform = c.transform 70 | 71 | tmpTransform.translate(newVector3(v.bounds.width/2, v.bounds.height/3 * 2, 0)) 72 | tmpTransform.rotateZ(-v.rotation) 73 | tmpTransform.translate(newVector3(-50, -50, 0)) 74 | 75 | c.fillColor = newColor(0.5, 0.5, 0) 76 | c.strokeWidth = 0 77 | c.withTransform tmpTransform: 78 | c.drawRoundedRect(newRect(0, 0, 100, 200), 20) 79 | 80 | c.strokeWidth = 10 81 | c.strokeColor = blackColor() 82 | c.fillColor = clearColor() 83 | c.drawArc(newPoint(100, 300), 50, v.rotation, v.rotation + Pi / 2) 84 | 85 | method viewWillMoveToWindow*(v: AnimationSampleView, w: Window) = 86 | if w.isNil: 87 | v.animation.cancel() 88 | else: 89 | w.addAnimation(v.animation) 90 | 91 | registerSample(AnimationSampleView, "Animation") 92 | -------------------------------------------------------------------------------- /test/sample05_fonts.nim: -------------------------------------------------------------------------------- 1 | import strutils 2 | import sample_registry 3 | import nimx / [ view, font, context, button, text_field, slider, popup_button ] 4 | 5 | type FontsView = ref object of View 6 | curFont: Font 7 | caption: string 8 | showBaseline: bool 9 | curFontSize: float 10 | baseline: Baseline 11 | 12 | template createSlider(fv: FontsView, title: string, y: var Coord, fr, to: Coord, val: typed) = 13 | let lb = newLabel(newRect(20, y, 120, 20)) 14 | lb.text = title & ":" 15 | let s = Slider.new(newRect(140, y, 120, 20)) 16 | let ef = newTextField(newRect(280, y, 120, 20)) 17 | s.onAction do(): 18 | let v = fr + (to - fr) * s.value 19 | ef.text = $v 20 | val = v 21 | fv.setNeedsDisplay() 22 | ef.onAction do(): 23 | try: 24 | let v = parseFloat(ef.text) 25 | s.value = (v - fr) / (to - fr) 26 | val = v 27 | fv.setNeedsDisplay() 28 | except: 29 | discard 30 | fv.addSubview(lb) 31 | fv.addSubview(s) 32 | fv.addSubview(ef) 33 | y += 22 34 | 35 | method init(v: FontsView, r: Rect) = 36 | procCall v.View.init(r) 37 | let captionTf = newTextField(newRect(20, 20, r.width - 40, 20)) 38 | captionTf.autoresizingMask = { afFlexibleWidth, afFlexibleMaxY } 39 | captionTf.text = "A Quick Brown $@#&¿" 40 | captionTf.onAction do(): 41 | v.caption = captionTf.text 42 | v.setNeedsDisplay() 43 | v.addSubview(captionTf) 44 | captionTf.sendAction() 45 | 46 | var y = 44.Coord 47 | v.createSlider("size", y, 8.0, 80.0, v.curFontSize) 48 | 49 | let showBaselineBtn = newCheckbox(newRect(20, y, 120, 16)) 50 | showBaselineBtn.title = "Show baseline" 51 | showBaselineBtn.onAction do(): 52 | v.showBaseline = showBaselineBtn.boolValue 53 | v.setNeedsDisplay() 54 | 55 | v.addSubview(showBaselineBtn) 56 | y += 16 + 5 57 | 58 | let baselineSelector = PopupButton.new(newRect(20, y, 120, 20)) 59 | var items = newSeq[string]() 60 | for i in Baseline.low .. Baseline.high: 61 | items.add($i) 62 | baselineSelector.items = items 63 | baselineSelector.onAction do(): 64 | v.baseline = Baseline(baselineSelector.selectedIndex) 65 | v.setNeedsDisplay() 66 | v.addSubview(baselineSelector) 67 | 68 | method draw(v: FontsView, r: Rect) = 69 | let c = currentContext() 70 | 71 | if v.curFont.isNil: 72 | v.curFont = systemFontOfSize(v.curFontSize) 73 | v.curFont.size = v.curFontSize 74 | 75 | let s = v.curFont.sizeOfString(v.caption) 76 | var origin = s.centerInRect(v.bounds) 77 | 78 | if v.showBaseline: 79 | c.fillColor = newGrayColor(0.5) 80 | c.drawRect(newRect(origin, newSize(s.width, 1))) 81 | 82 | c.fillColor = blackColor() 83 | let oldBaseline = v.curFont.baseline 84 | v.curFont.baseline = v.baseline 85 | c.drawText(v.curFont, origin, v.caption) 86 | v.curFont.baseline = oldBaseline 87 | 88 | registerSample(FontsView, "Fonts") 89 | -------------------------------------------------------------------------------- /test/sample06_timers.nim: -------------------------------------------------------------------------------- 1 | import strutils 2 | import sample_registry 3 | import nimx / [ view, timer, text_field, button ] 4 | 5 | type TimersSampleView = ref object of View 6 | timer: Timer 7 | intervalTextField: TextField 8 | 9 | method init(t: TimersSampleView, r: Rect) = 10 | procCall t.View.init(r) 11 | 12 | discard t.newLabel(newPoint(20, 20), newSize(120, 20), "interval: ") 13 | let intervalTextField = t.newTextField(newPoint(150, 20), newSize(120, 20), "5") 14 | 15 | discard t.newLabel(newPoint(20, 50), newSize(120, 20), "periodic: ") 16 | 17 | let periodicButton = newCheckbox(newRect(150, 50, 20, 20)) 18 | t.addSubview(periodicButton) 19 | 20 | var firesLabel: TextField 21 | 22 | let startButton = newButton(newRect(20, 80, 100, 20)) 23 | startButton.title = "Start" 24 | startButton.onAction do(): 25 | t.timer.clear() 26 | firesLabel.text = "fires: " 27 | t.timer = newTimer(parseFloat(intervalTextField.text), periodicButton.boolValue, proc() = 28 | firesLabel.text = firesLabel.text & "O" 29 | ) 30 | t.addSubview(startButton) 31 | 32 | let clearButton = newButton(newRect(20, 110, 100, 20)) 33 | clearButton.title = "Clear" 34 | clearButton.onAction do(): 35 | t.timer.clear() 36 | t.addSubview(clearButton) 37 | 38 | let pauseButton = newButton(newRect(20, 140, 100, 20)) 39 | pauseButton.title = "Pause" 40 | pauseButton.onAction do(): 41 | if not t.timer.isNil: 42 | t.timer.pause() 43 | t.addSubview(pauseButton) 44 | 45 | let resumeButton = newButton(newRect(20, 170, 100, 20)) 46 | resumeButton.title = "Resume" 47 | resumeButton.onAction do(): 48 | if not t.timer.isNil: 49 | t.timer.resume() 50 | t.addSubview(resumeButton) 51 | 52 | let secondsLabel = t.newLabel(newPoint(20, 200), newSize(120, 20), "seconds: ") 53 | var secs = 0 54 | setInterval 1.0, proc() = 55 | inc secs 56 | if secs >= 10: 57 | secs = 0 58 | secondsLabel.text = "seconds: " 59 | for i in 0 ..< secs: 60 | secondsLabel.text = secondsLabel.text & "O" 61 | 62 | firesLabel = t.newLabel(newPoint(20, 230), newSize(120, 20), "fires: ") 63 | 64 | registerSample(TimersSampleView, "Timers") 65 | -------------------------------------------------------------------------------- /test/sample07_collections.nim: -------------------------------------------------------------------------------- 1 | import strutils 2 | import sample_registry 3 | import nimx / [ collection_view, popup_button, slider, text_field, timer, view ] 4 | 5 | type CollectionsSampleView = ref object of View 6 | 7 | method init(v: CollectionsSampleView, r: Rect) = 8 | procCall v.View.init(r) 9 | 10 | setTimeout 0.2, proc() = 11 | let collection = @["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven"] 12 | 13 | let collectionView = newCollectionView(newRect(0, 0, 450, 250), newSize(50, 50), LayoutDirection.LeftToRight) 14 | collectionView.numberOfItems = proc(): int = 15 | return collection.len() 16 | collectionView.viewForItem = proc(i: int): View = 17 | result = newView(newRect(0, 0, 100, 100)) 18 | discard newLabel(result, newPoint(0, 0), newSize(50, 50), collection[i]) 19 | result.backgroundColor = newColor(1.0, 0.0, 0.0, 0.8) 20 | collectionView.itemSize = newSize(50, 50) 21 | collectionView.backgroundColor = newColor(1.0, 1.0, 1.0, 1.0) 22 | collectionView.updateLayout() 23 | 24 | v.addSubview(collectionView) 25 | 26 | discard newLabel(v, newPoint(470, 5), newSize(100, 10), "Layout direction:") 27 | 28 | let popupDirectionRule = newPopupButton(v, newPoint(470, 20), newSize(100, 20), ["LeftToRight", "TopDown"]) 29 | popupDirectionRule.onAction do(): 30 | collectionView.layoutDirection = popupDirectionRule.selectedIndex().LayoutDirection 31 | collectionView.updateLayout() 32 | 33 | discard newLabel(v, newPoint(470, 45), newSize(100, 10), "Layout width:") 34 | 35 | let popupLayoutWidth = newPopupButton(v, newPoint(470, 60), newSize(100, 20), ["Auto", "1", "2", "3", "4"]) 36 | popupLayoutWidth.onAction do(): 37 | collectionView.layoutWidth = popupLayoutWidth.selectedIndex() 38 | collectionView.updateLayout() 39 | 40 | discard newLabel(v, newPoint(470, 85), newSize(100, 10), "Item size:") 41 | 42 | let popupItemSize = newPopupButton(v, newPoint(470, 100), newSize(100, 20), ["50", "100", "150"]) 43 | popupItemSize.onAction do(): 44 | collectionView.itemSize = newSize((50 + 50 * popupItemSize.selectedIndex()).Coord, (50 + 50 * popupItemSize.selectedIndex()).Coord) 45 | collectionView.updateLayout() 46 | 47 | discard newLabel(v, newPoint(470, 125), newSize(100, 10), "Offset:") 48 | 49 | let offsetField = newTextField(newRect(newPoint(470, 140), newSize(100, 20))) 50 | offsetField.continuous = true 51 | offsetField.onAction do(): 52 | var offset = try: parseFloat(offsetField.text) except: 0.0 53 | collectionView.offset = offset 54 | collectionView.updateLayout() 55 | v.addSubview(offsetField) 56 | 57 | 58 | registerSample(CollectionsSampleView, "Collections") 59 | -------------------------------------------------------------------------------- /test/sample09_docking_tabs.nim: -------------------------------------------------------------------------------- 1 | import random 2 | import sample_registry 3 | import nimx / [ view, linear_layout, button ] 4 | import nimx/editor/tab_view 5 | 6 | type DockingTabsSampleView = ref object of View 7 | tabNameIndex: int 8 | 9 | proc newTabTitle(v: DockingTabsSampleView): string = 10 | inc v.tabNameIndex 11 | result = "Tab " & $v.tabNameIndex 12 | 13 | proc newRandomColor(): Color = newColor(rand(1.0), rand(1.0), rand(1.0), 1.0) 14 | 15 | proc newTab(v: DockingTabsSampleView): View {.gcsafe.} = 16 | result = View.new(newRect(0, 0, 100, 100)) 17 | result.backgroundColor = newRandomColor() 18 | 19 | const buttonSize = 20 20 | let pane = result 21 | 22 | proc indexOfPaneInTabView(): int = 23 | let tv = TabView(pane.superview) 24 | for i in 0 ..< tv.tabsCount: 25 | if tv.viewOfTab(i) == pane: 26 | return i 27 | result = -1 28 | 29 | let addButton = Button.new(newRect(5, 5, buttonSize, buttonSize)) 30 | addButton.title = "+" 31 | addButton.onAction do(): 32 | let tv = TabView(pane.superview) 33 | let i = indexOfPaneInTabView() + 1 34 | tv.insertTab(i, v.newTabTitle(), v.newTab()) 35 | tv.selectTab(i) 36 | result.addSubview(addButton) 37 | 38 | let removeButton = Button.new(newRect(addButton.frame.maxX + 2, 5, buttonSize, buttonSize)) 39 | removeButton.title = "-" 40 | removeButton.onAction do(): 41 | let tv = TabView(pane.superview) 42 | if tv.tabsCount == 1: 43 | tv.removeFromSplitViewSystem() 44 | else: 45 | tv.removeTab(indexOfPaneInTabView()) 46 | result.addSubview(removeButton) 47 | 48 | let c = Button.new(newRect(removeButton.frame.maxX + 2, 5, buttonSize, buttonSize)) 49 | c.title = "c" 50 | c.onAction do(): 51 | pane.backgroundColor = newRandomColor() 52 | result.addSubview(c) 53 | 54 | method init(v: DockingTabsSampleView, r: Rect) = 55 | procCall v.View.init(r) 56 | let pane = TabView.new(v.bounds) 57 | pane.dockingTabs = true 58 | pane.addTab(v.newTabTitle(), v.newTab()) 59 | pane.resizingMask = "wh" 60 | pane.userConfigurable = true 61 | v.addSubview(pane) 62 | 63 | registerSample(DockingTabsSampleView, "Docking Tabs") 64 | -------------------------------------------------------------------------------- /test/sample10_text.nim: -------------------------------------------------------------------------------- 1 | import strutils 2 | import sample_registry 3 | import nimx / [ view, font, context, button, text_field, slider, popup_button, 4 | formatted_text, segmented_control, scroll_view ] 5 | 6 | type TextView = ref object of View 7 | text: FormattedText 8 | 9 | type TextSampleView = ref object of View 10 | 11 | const textSample = """Nim is statically typed, with a simple syntax. It supports compile-time metaprogramming features such as syntactic macros and term rewriting macros. 12 | Term rewriting macros enable library implementations of common data structures such as bignums and matrixes to be implemented with an efficiency as if they would have been builtin language facilities. 13 | Iterators are supported and can be used as first class entities in the language as can functions, these features allow for functional programming to be used. 14 | Object-oriented programming is supported by inheritance and multiple dispatch. Functions can be generic and can also be overloaded, generics are further enhanced by the support for type classes. 15 | Operator overloading is also supported. Nim includes automatic garbage collection based on deferred reference counting with cycle detection. 16 | Andrew Binstock (editor-in-chief of Dr. Dobb's) says Nim (formerly known as Nimrod) "presents a most original design that straddles Pascal and Python and compiles to C code or JavaScript. 17 | And realistic Soft Shadow :)""" 18 | 19 | iterator rangesOfSubstring(haystack, needle: string): (int, int) = 20 | var start = 0 21 | while true: 22 | let index = haystack.find(needle, start) 23 | if index == -1: 24 | break 25 | else: 26 | let b = index + needle.len 27 | yield (index, b) 28 | start = b 29 | 30 | method init(v: TextSampleView, r: Rect) = 31 | procCall v.View.init(r) 32 | 33 | let tv = TextField.new(v.bounds.inset(50, 50)) 34 | tv.resizingMask = "wh" 35 | tv.text = textSample 36 | tv.backgroundColor = newColor(0.5, 0, 0, 0.5) 37 | tv.multiline = true 38 | 39 | for a, b in tv.text.rangesOfSubstring("Nim"): 40 | tv.formattedText.setFontInRange(a, b, systemFontOfSize(40)) 41 | tv.formattedText.setStrokeInRange(a, b, newColor(1, 0, 0), 5) 42 | 43 | for a, b in tv.text.rangesOfSubstring("programming"): 44 | tv.formattedText.setTextColorInRange(a, b, newColor(1, 0, 0)) 45 | tv.formattedText.setShadowInRange(a, b, newGrayColor(0.5, 0.5), newSize(2, 3), 0.0, 0.0) 46 | 47 | for a, b in tv.text.rangesOfSubstring("supported"): 48 | tv.formattedText.setTextColorInRange(a, b, newColor(0, 0.6, 0)) 49 | 50 | for a, b in tv.text.rangesOfSubstring("Soft Shadow"): 51 | tv.formattedText.setFontInRange(a, b, systemFontOfSize(40)) 52 | tv.formattedText.setShadowInRange(a, b, newColor(0.0, 0.0, 1.0, 1.0), newSize(2, 3), 5.0, 0.8) 53 | 54 | let sv = newScrollView(tv) 55 | v.addSubview(sv) 56 | 57 | let hAlignChooser = SegmentedControl.new(newRect(5, 5, 200, 25)) 58 | hAlignChooser.segments = @[$haLeft, $haCenter, $haRight] 59 | v.addSubview(hAlignChooser) 60 | hAlignChooser.onAction do(): 61 | tv.formattedText.horizontalAlignment = parseEnum[HorizontalTextAlignment](hAlignChooser.segments[hAlignChooser.selectedSegment]) 62 | 63 | let vAlignChooser = SegmentedControl.new(newRect(hAlignChooser.frame.maxX + 5, 5, 200, 25)) 64 | vAlignChooser.segments = @[$vaTop, $vaCenter, $vaBottom] 65 | vAlignChooser.selectedSegment = 0 66 | v.addSubview(vAlignChooser) 67 | vAlignChooser.onAction do(): 68 | tv.formattedText.verticalAlignment = parseEnum[VerticalAlignment](vAlignChooser.segments[vAlignChooser.selectedSegment]) 69 | tv.formattedText.verticalAlignment = vaTop 70 | 71 | method draw(v: TextView, r: Rect) = 72 | procCall v.View.draw(r) 73 | let c = currentContext() 74 | v.text.boundingSize = v.bounds.size 75 | c.drawText(newPoint(0, 0), v.text) 76 | 77 | registerSample(TextSampleView, "Text") 78 | -------------------------------------------------------------------------------- /test/sample11_expanded_views.nim: -------------------------------------------------------------------------------- 1 | import random 2 | import sample_registry 3 | import nimx / [ view, font, context, button, expanding_view, stack_view ] 4 | 5 | type ExpandingSampleView = ref object of View 6 | welcomeFont: Font 7 | 8 | method init(v: ExpandingSampleView, r: Rect) = 9 | procCall v.View.init(r) 10 | 11 | let stackView = newStackView(newRect(90,50, 300, 600)) 12 | v.addSubview(stackView) 13 | 14 | for i in 0..4: 15 | let rand_y = rand(100 .. 400) 16 | let expView = newExpandingView(newRect(0, 0, 300, rand_y.Coord), true) 17 | expView.title = "newExpandedView " & $i 18 | stackView.addSubview(expView) 19 | 20 | for i in 0..4: 21 | let rand_y = rand(0 .. 300) 22 | let expView1 = newExpandingView(newRect(0, 0, 300, 10), true) 23 | expView1.title = "WOW " & $i 24 | expView.addContent(expView1) 25 | 26 | let testView = newView(newRect(0,0, 100, rand_y.Coord)) 27 | testView.backgroundColor = newColor(0.2, 1.2, 0.2, 1.0) 28 | expView1.addContent(testView) 29 | discard newButton(testView, newPoint(10, 10), newSize(16, 16), "X") 30 | 31 | method draw(v: ExpandingSampleView, r: Rect) = 32 | let c = currentContext() 33 | if v.welcomeFont.isNil: 34 | v.welcomeFont = systemFontOfSize(20) 35 | c.fillColor = blackColor() 36 | c.drawText(v.welcomeFont, newPoint(10, 5), "test") 37 | 38 | registerSample(ExpandingSampleView, "ExpandingView") 39 | -------------------------------------------------------------------------------- /test/sample12_menus.nim: -------------------------------------------------------------------------------- 1 | import sample_registry 2 | import nimx / [ view, menu, button, text_field ] 3 | 4 | type MenuSampleView = ref object of View 5 | 6 | proc leftOf(v: View, width: Coord): Rect = 7 | let f = v.frame 8 | result.origin.x = f.maxX + 5 9 | result.origin.y = f.y 10 | result.size.height = f.height 11 | result.size.width = width 12 | 13 | method init(v: MenuSampleView, r: Rect) = 14 | procCall v.View.init(r) 15 | let b = Button.new(newRect(5, 5, 100, 25)) 16 | b.title = "Menu" 17 | 18 | let textField = TextField.new(b.leftOf(120)) 19 | textField.text = "Menu: none" 20 | 21 | let m = makeMenu("File"): 22 | - "Open": 23 | textField.text = "Menu: Open" 24 | echo "Open" 25 | - "Save": 26 | textField.text = "Menu: Save" 27 | - "-" 28 | + "Bye": 29 | - "Sub1" 30 | - "-" 31 | - "Sub2": 32 | textField.text = "Menu: Sub2" 33 | 34 | b.onAction do(): 35 | m.popupAtPoint(b, newPoint(0, 25)) 36 | v.addSubview(b) 37 | v.addSubview(textField) 38 | 39 | registerSample(MenuSampleView, "Menus") 40 | -------------------------------------------------------------------------------- /test/sample13_drag_and_drop.nim: -------------------------------------------------------------------------------- 1 | import variant 2 | import sample_registry 3 | import nimx / [ view, view_event_handling, drag_and_drop, text_field, expanding_view, 4 | view_render_to_image ] 5 | import nimx/pasteboard/pasteboard_item 6 | 7 | type DragAndDropView = ref object of View 8 | type MyDropDelegate* = ref object of DragDestinationDelegate 9 | type DraggedView* = ref object of View 10 | 11 | const PboardSampleDrag* = "nimx.sample.drag" 12 | 13 | method onTouchEv*(v: DraggedView, e: var Event): bool = 14 | if e.buttonState == bsDown: 15 | let dpi = newPasteboardItem(PboardSampleDrag, v.name) 16 | let image = v.screenShot() 17 | startDrag(dpi, image) 18 | 19 | #============= MyDropDelegate ============== 20 | 21 | method onDragEnter*(dd: MyDropDelegate, target: View, i: PasteboardItem) = 22 | target.backgroundColor.a = 0.5 23 | let label = target.subviews[1].TextField 24 | label.text = "drag over: " & i.data 25 | 26 | method onDragExit*(dd: MyDropDelegate, target: View, i: PasteboardItem) = 27 | target.backgroundColor.a = 1.0 28 | let label = target.subviews[1].TextField 29 | label.text = "drag over: " 30 | 31 | method onDrop*(dd: MyDropDelegate, target: View, i: PasteboardItem) = 32 | let label = target.subviews[1].TextField 33 | label.text = "drag over: " 34 | 35 | if i.data == "yellow": 36 | target.backgroundColor = newColor(1.0, 1.0, 0.0, 1.0) 37 | if i.data == "green": 38 | target.backgroundColor = newColor(0.0, 1.0, 0.0, 1.0) 39 | 40 | #============= Views ============== 41 | 42 | proc createDraggedView(pos: Point, name: string): View = 43 | result = DraggedView.new(newRect(pos.x, pos.y, 150, 60)) 44 | result.name = name 45 | result.backgroundColor = newColor(0.0, 1.0, 0.0, 1.0) 46 | 47 | let label_name = newLabel(newRect(2, 0, 200, 40)) 48 | label_name.text = result.name 49 | result.addSubView(label_name) 50 | 51 | proc createDropView(pos: Point, name: string, delegate: MyDropDelegate): View = 52 | result = newView(newRect(pos.x, pos.y, 200, 200)) 53 | result.name = name 54 | result.backgroundColor = newColor(1.0, 0.0, 0.0, 1.0) 55 | result.dragDestination = delegate 56 | 57 | let label_name = newLabel(newRect(2, 150, 200, 40)) 58 | label_name.text = result.name 59 | result.addSubView(label_name) 60 | 61 | let label_drop = newLabel(newRect(2, 170, 200, 35)) 62 | label_drop.text = "drop : " 63 | result.addSubView(label_drop) 64 | 65 | method init(v: DragAndDropView, r: Rect) = 66 | procCall v.View.init(r) 67 | 68 | let dropDelegate = MyDropDelegate.new() 69 | let red_view = createDropView(newPoint(50.0, 80.0), "red_drop_view", dropDelegate) 70 | 71 | let blue_view = createDropView(newPoint(350.0, 80.0), "blue_drop_view", dropDelegate) 72 | blue_view.backgroundColor = newColor(0.0, 0.0, 1.0, 1.0) 73 | 74 | v.addSubView(red_view) 75 | v.addSubView(blue_view) 76 | 77 | let draggedView1 = createDraggedView(newPoint(50, 10), "green") 78 | v.addSubView(draggedView1) 79 | 80 | let draggedView2 = createDraggedView(newPoint(350, 10), "yellow") 81 | draggedView2.backgroundColor = newColor(1.0, 1.0, 0.0, 1.0) 82 | v.addSubView(draggedView2) 83 | 84 | let expView = newExpandingView(newRect(50, 300, 200, 400), true) 85 | expView.title = "Expanded View " 86 | v.addSubview(expView) 87 | 88 | let exp_drop_view = createDropView(newPoint(350.0, 80.0), "exp_drop_view", dropDelegate) 89 | exp_drop_view.backgroundColor = newColor(1.0, 0.0, 1.0, 1.0) 90 | expView.addContent(exp_drop_view) 91 | 92 | registerSample(DragAndDropView, "DragAndDrop") 93 | -------------------------------------------------------------------------------- /test/sample16_outline.nim: -------------------------------------------------------------------------------- 1 | import sample_registry 2 | import nimx / [ view, outline_view, scroll_view, table_view_cell, text_field ] 3 | import variant 4 | 5 | type 6 | DataItem = ref object 7 | name: string 8 | children: seq[DataItem] 9 | parent: DataItem 10 | 11 | type OutlineSampleView = ref object of View 12 | 13 | proc addChild(p: DataItem, ch: DataItem): DataItem = 14 | p.children.add(ch) 15 | ch.parent = p 16 | result = p 17 | 18 | method init*(v: OutlineSampleView, r: Rect) = 19 | procCall v.View.init(r) 20 | 21 | var outline = OutlineView.new(newRect(10.0, 10.0, 200.0, v.bounds.height - 10.0)) 22 | outline.autoresizingMask={afFlexibleWidth, afFlexibleHeight} 23 | 24 | var scroll = newScrollView(outline) 25 | scroll.autoresizingMask={afFlexibleWidth, afFlexibleHeight} 26 | v.addSubview(scroll) 27 | 28 | var rootDataItem = DataItem(name: "root") 29 | .addChild(DataItem(name: "child0")) 30 | .addChild( 31 | DataItem(name: "child1") 32 | .addChild(DataItem(name: "subchild0")) 33 | .addChild(DataItem(name: "subchild1")) 34 | ) 35 | .addChild(DataItem(name: "child2")) 36 | 37 | outline.numberOfChildrenInItem = proc(item: Variant, indexPath: openArray[int]): int = 38 | if indexPath.len == 0: 39 | return 1 40 | else: 41 | return item.get(DataItem).children.len 42 | 43 | outline.childOfItem = proc(item: Variant, indexPath: openArray[int]): Variant = 44 | if indexPath.len == 1: 45 | return newVariant(rootDataItem) 46 | else: 47 | let node = item.get(DataItem).children[indexPath[^1]] 48 | return newVariant(node) 49 | 50 | outline.createCell = proc(): TableViewCell = 51 | result = newTableViewCell( 52 | newLabel(newRect(0, 0, 200, 20)) 53 | ) 54 | 55 | outline.configureCell = proc(cell: TableViewCell, indexPath: openArray[int]) = 56 | let node = outline.itemAtIndexPath(indexPath).get(DataItem) 57 | let textField = TextField(cell.subviews[0]) 58 | textField.text = node.name 59 | 60 | outline.onDragAndDrop = proc(fromPath, toPath: openArray[int]) = 61 | echo fromPath, " >> ", toPath 62 | var parentPos = @toPath[0..^2] 63 | let fromNode = outline.itemAtIndexPath(fromPath).get(DataItem) 64 | let toNode = outline.itemAtIndexPath(parentPos).get(DataItem) 65 | 66 | let fi = fromNode.parent.children.find(fromNode) 67 | fromNode.parent.children.delete(fi) 68 | fromNode.parent = toNode 69 | let toIndex = clamp(toPath[^1], 0, toNode.children.len) 70 | toNode.children.insert(fromNode, toIndex) 71 | outline.reloadData() 72 | 73 | outline.onSelectionChanged = proc() = 74 | let ip = outline.selectedIndexPath 75 | let node = if ip.len > 0: 76 | outline.itemAtIndexPath(ip).get(DataItem) 77 | else: 78 | nil 79 | if node.isNil: 80 | echo "select nil" 81 | else: 82 | echo "select ", node.name 83 | 84 | outline.reloadData() 85 | 86 | 87 | method draw(v: OutlineSampleView, r: Rect) = 88 | v.setNeedsDisplay() 89 | 90 | 91 | registerSample(OutlineSampleView, "Outline") 92 | -------------------------------------------------------------------------------- /test/sample_registry.nim: -------------------------------------------------------------------------------- 1 | import typetraits 2 | 3 | type SampleInfo = tuple[name: string, className: string] 4 | 5 | var allSamples* {.threadvar.}: seq[SampleInfo] 6 | 7 | template registerSample*(T: typedesc, sampleName: string) = 8 | allSamples.add((sampleName, name(T))) 9 | registerClass(T) 10 | --------------------------------------------------------------------------------