diff --git a/.editorconfig b/.editorconfig index 7278fb63..f942842a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,3 +6,6 @@ indent_size = 2 indent_style = space insert_final_newline = true trim_trailing_whitespace = true + +[meson.build] +indent_size = 4 diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 00000000..41f66c7c --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,35 @@ +"Category: CI": + - .github/workflows/* + +"Category: Meta": + - ./* + - .github/* + - .github/ISSUE_TEMPLATE/* + - .github/PULL_REQUEST_TEMPLATE/* + - .gitignore + +"Category: Build System": + - meson.build + - meson_options.txt + - subprojects/* + +"Category: Documentation": + - docs/**/* + +"Category: Resources": + - resources/**/* + +"Category: Themes": + - data/colors/* + +"Category: Lua Core": + - data/core/**/* + +"Category: Fonts": + - data/fonts/* + +"Category: Plugins": + - data/plugins/* + +"Category: C Core": + - src/**/* diff --git a/.github/workflows/auto_labeler_pr.yml b/.github/workflows/auto_labeler_pr.yml new file mode 100644 index 00000000..48ea1eeb --- /dev/null +++ b/.github/workflows/auto_labeler_pr.yml @@ -0,0 +1,16 @@ +name: "Pull Request Labeler" +on: +- pull_request_target + +permissions: + pull-requests: write + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Apply Type Label + uses: actions/labeler@v3 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + sync-labels: "" # works around actions/labeler#104 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0ef07952..2778de79 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,16 +1,44 @@ name: CI +# All builds use lhelper only for releases, +# otherwise for normal builds dependencies are dynamically linked. + on: push: branches: - - '*' + - '*' +# tags: +# - 'v[0-9]*' pull_request: branches: - - '*' + - '*' jobs: - build-linux: - name: Build Linux + archive_source_code: + name: Source Code Tarball + runs-on: ubuntu-18.04 + # Only on tags/releases + if: startsWith(github.ref, 'refs/tags/') + steps: + - uses: actions/checkout@v2 + - name: Python Setup + uses: actions/setup-python@v2 + with: + python-version: 3.6 + - name: Install Dependencies + run: | + sudo apt-get install -qq ninja-build + pip3 install meson + - name: Package + shell: bash + run: bash scripts/package.sh --version ${GITHUB_REF##*/} --debug --source + - uses: actions/upload-artifact@v2 + with: + name: Source Code Tarball + path: "lite-xl-*-src.tar.gz" + + build_linux: + name: Linux runs-on: ubuntu-18.04 strategy: matrix: @@ -21,48 +49,204 @@ jobs: CC: ${{ matrix.config.cc }} CXX: ${{ matrix.config.cxx }} steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.6 - - name: Install dependencies - run: | - sudo apt-get install -qq libsdl2-dev libfreetype6 ninja-build - pip3 install meson - - name: Build package - run: bash build-packages.sh x86-64 - - name: upload packages - uses: actions/upload-artifact@v2 - with: - name: Ubuntu Package - path: lite-xl-linux-*.tar.gz + - name: Set Environment Variables + if: ${{ matrix.config.cc == 'gcc' }} + run: | + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV" + echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-linux-$(uname -m)" >> "$GITHUB_ENV" + - uses: actions/checkout@v2 + - name: Python Setup + uses: actions/setup-python@v2 + with: + python-version: 3.6 + - name: Update Packages + run: sudo apt-get update + - name: Install Dependencies + if: ${{ !startsWith(github.ref, 'refs/tags/') }} + run: bash scripts/install-dependencies.sh --debug + - name: Install Release Dependencies + if: ${{ startsWith(github.ref, 'refs/tags/') }} + run: | + bash scripts/install-dependencies.sh --debug --lhelper + bash scripts/lhelper.sh --debug + - name: Build + run: | + bash --version + bash scripts/build.sh --debug --forcefallback + - name: Package + if: ${{ matrix.config.cc == 'gcc' }} + run: bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --binary + - name: AppImage + if: ${{ matrix.config.cc == 'gcc' && startsWith(github.ref, 'refs/tags/') }} + run: bash scripts/appimage.sh --nobuild --version ${INSTALL_REF} + - name: Upload Artifacts + uses: actions/upload-artifact@v2 + if: ${{ matrix.config.cc == 'gcc' }} + with: + name: Linux Artifacts + path: | + ${{ env.INSTALL_NAME }}.tar.gz + LiteXL-${{ env.INSTALL_REF }}-x86_64.AppImage - build-macox: - name: Build Mac OS X + build_macos: + name: macOS (x86_64) runs-on: macos-10.15 + env: + CC: clang + CXX: clang++ + steps: + - name: System Information + run: | + system_profiler SPSoftwareDataType + bash --version + gcc -v + xcodebuild -version + - name: Set Environment Variables + run: | + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV" + echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-macos-$(uname -m)" >> "$GITHUB_ENV" + - uses: actions/checkout@v2 + - name: Python Setup + uses: actions/setup-python@v2 + with: + python-version: 3.9 + - name: Install Dependencies + if: ${{ !startsWith(github.ref, 'refs/tags/') }} + run: bash scripts/install-dependencies.sh --debug + - name: Install Release Dependencies + if: ${{ startsWith(github.ref, 'refs/tags/') }} + run: | + bash scripts/install-dependencies.sh --debug --lhelper + bash scripts/lhelper.sh --debug + - name: Build + run: | + bash --version + bash scripts/build.sh --bundle --debug --forcefallback + - name: Error Logs + if: failure() + run: | + mkdir ${INSTALL_NAME} + cp /usr/var/lhenv/lite-xl/logs/* ${INSTALL_NAME} + tar czvf ${INSTALL_NAME}.tar.gz ${INSTALL_NAME} +# - name: Package +# if: ${{ !startsWith(github.ref, 'refs/tags/') }} +# run: bash scripts/package.sh --version ${INSTALL_REF} --debug --addons + - name: Create DMG Image + run: bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --dmg + - name: Upload DMG Image + uses: actions/upload-artifact@v2 + with: + name: macOS DMG Image + path: ${{ env.INSTALL_NAME }}.dmg + - name: Upload Error Logs + uses: actions/upload-artifact@v2 + if: failure() + with: + name: Error Logs + path: ${{ env.INSTALL_NAME }}.tar.gz + + build_windows_msys2: + name: Windows + runs-on: windows-2019 strategy: matrix: - config: - # - { name: "GCC", cc: gcc-10, cxx: g++-10 } - - { name: "clang", cc: clang, cxx: clang++ } - env: - CC: ${{ matrix.config.cc }} - CXX: ${{ matrix.config.cxx }} + msystem: [MINGW32, MINGW64] + defaults: + run: + shell: msys2 {0} steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.9 - - name: Install dependencies - run: | - pip3 install meson - brew install ninja sdl2 - - name: Build package - run: bash build-packages.sh x86-64 - - name: upload packages - uses: actions/upload-artifact@v2 - with: - name: Mac OS X Package - path: lite-xl-macosx-*.zip + - uses: actions/checkout@v2 + - uses: msys2/setup-msys2@v2 + with: + #msystem: MINGW64 + msystem: ${{ matrix.msystem }} + update: true + install: >- + base-devel + git + zip + - name: Set Environment Variables + run: | + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + echo "INSTALL_NAME=lite-xl-${GITHUB_REF##*/}-windows-$(uname -m)" >> "$GITHUB_ENV" + echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV" + - name: Install Dependencies + if: ${{ !startsWith(github.ref, 'refs/tags/') }} + run: bash scripts/install-dependencies.sh --debug + - name: Install Release Dependencies + if: ${{ startsWith(github.ref, 'refs/tags/') }} + run: bash scripts/install-dependencies.sh --debug --lhelper + - name: Build + run: | + bash --version + bash scripts/build.sh --debug --forcefallback + - name: Error Logs + if: failure() + run: | + mkdir ${INSTALL_NAME} + cp /usr/var/lhenv/lite-xl/logs/* ${INSTALL_NAME} + tar czvf ${INSTALL_NAME}.tar.gz ${INSTALL_NAME} + - name: Package + run: bash scripts/package.sh --version ${INSTALL_REF} --debug --addons --binary + - name: Build Installer + if: ${{ startsWith(github.ref, 'refs/tags/') }} + run: bash scripts/innosetup/innosetup.sh --debug + - name: Upload Artifacts + uses: actions/upload-artifact@v2 + with: + name: Windows Artifacts + path: | + LiteXL*.exe + ${{ env.INSTALL_NAME }}.zip + - name: Upload Error Logs + uses: actions/upload-artifact@v2 + if: failure() + with: + name: Error Logs + path: ${{ env.INSTALL_NAME }}.tar.gz + + deploy: + name: Deployment + runs-on: ubuntu-18.04 +# if: startsWith(github.ref, 'refs/tags/') + if: false + needs: + - archive_source_code + - build_linux + - build_macos + - build_windows_msys2 + steps: + - name: Set Environment Variables + run: echo "INSTALL_REF=${GITHUB_REF##*/}" >> "$GITHUB_ENV" + - uses: actions/download-artifact@v2 + with: + name: Linux Artifacts + - uses: actions/download-artifact@v2 + with: + name: macOS DMG Image + - uses: actions/download-artifact@v2 + with: + name: Source Code Tarball + - uses: actions/download-artifact@v2 + with: + name: Windows Artifacts + - name: Display File Information + shell: bash + run: ls -lR + # Note: not using `actions/create-release@v1` + # because it cannot update an existing release + # see https://github.com/actions/create-release/issues/29 + - uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: ${{ env.INSTALL_REF }} + name: Release ${{ env.INSTALL_REF }} + draft: false + prerelease: false + files: | + lite-xl-${{ env.INSTALL_REF }}-* + LiteXL*.AppImage + LiteXL*.exe diff --git a/.gitignore b/.gitignore index 42b4ba06..41183a14 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,20 @@ -build* -.build* -.run* -*.zip -*.tar.gz -.lite-debug.log -subprojects/lua -subprojects/libagg -subprojects/reproc -lite-xl -error.txt +build*/ +.build*/ +lhelper/ +submodules/ +subprojects/lua/ +subprojects/reproc/ +/appimage* .ccls-cache +.lite-debug.log +.run* +*.diff +*.exe +*.tar.gz +*.zip +*.DS_Store +*App* compile_commands.json +error.txt +lite-xl* +LiteXL* diff --git a/README.md b/README.md index b96c9dca..e6cd18dc 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,18 @@ # Lite XL +[![CI]](https://github.com/lite-xl/lite-xl/actions/workflows/build.yml) [![Discord Badge Image]](https://discord.gg/RWzqC3nx7K) ![screenshot-dark] A lightweight text editor written in Lua, adapted from [lite]. -* **[Get Lite XL]** — Download for Windows, Linux and Mac OS (notarized app). +* **[Get Lite XL]** — Download for Windows, Linux and Mac OS. * **[Get plugins]** — Add additional functionality, adapted for Lite XL. * **[Get color themes]** — Add additional colors themes. Please refer to our [website] for the user and developer documentation, -including [build] instructions. +including [build] instructions details. A quick build guide is described below. Lite XL has support for high DPI display on Windows and Linux and, since 1.16.7 release, it supports **retina displays** on macOS. @@ -42,6 +43,91 @@ the [plugins repository] or in the [Lite XL plugins repository]. Additional color themes can be found in the [colors repository]. These color themes are bundled with all releases of Lite XL by default. +## Quick Build Guide + +If you compile Lite XL yourself, it is recommended to use the script +`build-packages.sh`: + +```sh +bash build-packages.sh -h +``` + +The script will run Meson and create a tar compressed archive with the application or, +for Windows, a zip file. Lite XL can be easily installed +by unpacking the archive in any directory of your choice. + +Otherwise the following is an example of basic commands if you want to customize +the build: + +```sh +meson setup --buildtype=release --prefix build +meson compile -C build +DESTDIR="$(pwd)/lite-xl" meson install --skip-subprojects -C build +``` + +where `` might be one of `/`, `/usr` or `/opt`, the default is `/`. +To build a bundle application on macOS: + +```sh +meson setup --buildtype=release --Dbundle=true --prefix / build +meson compile -C build +DESTDIR="$(pwd)/Lite XL.app" meson install --skip-subprojects -C build +``` + +Please note that the package is relocatable to any prefix and the option prefix +affects only the place where the application is actually installed. + +## Installing Prebuilt + +Head over to [releases](https://github.com/lite-xl/lite-xl/releases) and download the version for your operating system. + +### Linux + +Unzip the file and `cd` into the `lite-xl` directory: + +```sh +tar -xzf +cd lite-xl +``` + +To run lite-xl without installing: +```sh +cd bin +./lite-xl +``` + +To install lite-xl copy files over into appropriate directories: + +```sh +mkdir -p $HOME/.local/bin && cp bin/lite-xl $HOME/.local/bin +cp -r share $HOME/.local +``` + +If `$HOME/.local/bin` is not in PATH: + +```sh +echo -e 'export PATH=$PATH:$HOME/.local/bin' >> $HOME/.bashrc +``` + +To get the icon to show up in app launcher: + +```sh +xdg-desktop-menu forceupdate +``` + +You may need to logout and login again to see icon in app launcher. + +To uninstall just run: + +```sh +rm -f $HOME/.local/bin/lite-xl +rm -rf $HOME/.local/share/icons/hicolor/scalable/apps/lite-xl.svg \ + $HOME/.local/share/applications/org.lite_xl.lite_xl.desktop \ + $HOME/.local/share/metainfo/org.lite_xl.lite_xl.appdata.xml \ + $HOME/.local/share/lite-xl +``` + + ## Contributing Any additional functionality that can be added through a plugin should be done @@ -60,11 +146,12 @@ the terms of the MIT license. See [LICENSE] for details. See the [licenses] file for details on licenses used by the required dependencies. +[CI]: https://github.com/lite-xl/lite-xl/actions/workflows/build.yml/badge.svg [Discord Badge Image]: https://img.shields.io/discord/847122429742809208?label=discord&logo=discord [screenshot-dark]: https://user-images.githubusercontent.com/433545/111063905-66943980-84b1-11eb-9040-3876f1133b20.png [lite]: https://github.com/rxi/lite [website]: https://lite-xl.github.io -[build]: https://lite-xl.github.io/en/build +[build]: https://lite-xl.github.io/en/documentation/build/ [Get Lite XL]: https://github.com/franko/lite-xl/releases/latest [Get plugins]: https://github.com/franko/lite-plugins [Get color themes]: https://github.com/rxi/lite-colors diff --git a/build-packages.sh b/build-packages.sh index 3701ac53..4ecda0a0 100755 --- a/build-packages.sh +++ b/build-packages.sh @@ -1,216 +1,164 @@ #!/bin/bash +set -e -# strip-components is normally set to 1 to strip the initial "data" from the -# directory path. -copy_directory_from_repo () { - local tar_options=() - if [[ $1 == --strip-components=* ]]; then - tar_options+=($1) - shift - fi - local dirname="$1" - local destdir="$2" - git archive "$use_branch" "$dirname" --format=tar | tar xf - -C "$destdir" "${tar_options[@]}" +if [ ! -e "src/api/api.h" ]; then + echo "Please run this script from the root directory of Lite XL."; exit 1 +fi + +source scripts/common.sh + +show_help() { + echo + echo "Usage: $0 " + echo + echo "Common options:" + echo + echo "-h --help Show this help and exit." + echo "-b --builddir DIRNAME Set the name of the build directory (not path)." + echo " Default: '$(get_default_build_dir)'." + echo "-p --prefix PREFIX Install directory prefix." + echo " Default: '/'." + echo " --debug Debug this script." + echo + echo "Build options:" + echo + echo "-f --forcefallback Force to build subprojects dependencies statically." + echo "-B --bundle Create an App bundle (macOS only)" + echo "-P --portable Create a portable package." + echo "-O --pgo Use profile guided optimizations (pgo)." + echo " Requires running the application iteractively." + echo + echo "Package options:" + echo + echo "-d --destdir DIRNAME Set the name of the package directory (not path)." + echo " Default: 'lite-xl'." + echo "-v --version VERSION Sets the version on the package name." + echo "-A --appimage Create an AppImage (Linux only)." + echo "-D --dmg Create a DMG disk image (macOS only)." + echo " Requires NPM and AppDMG." + echo "-I --innosetup Create an InnoSetup installer (Windows only)." + echo "-S --source Create a source code package," + echo " including subprojects dependencies." + echo } -# Check if build directory is ok to be used to build. -build_dir_is_usable () { - local build="$1" - if [[ $build == */* || -z "$build" ]]; then - echo "invalid build directory, no path allowed: \"$build\"" - return 1 - fi - git ls-files --error-unmatch "$build" &> /dev/null - if [ $? == 0 ]; then - echo "invalid path, \"$build\" is under revision control" - return 1 - fi -} +main() { + local build_dir + local build_dir_option=() + local dest_dir + local dest_dir_option=() + local prefix + local prefix_option=() + local version + local version_option=() + local debug + local force_fallback + local appimage + local bundle + local innosetup + local portable + local pgo -# Ordinary release build -lite_build () { - local build="$1" - build_dir_is_usable "$build" || exit 1 - rm -fr "$build" - meson setup --buildtype=release "$build" || exit 1 - ninja -C "$build" || exit 1 -} - -# Build using Profile Guided Optimizations (PGO) -lite_build_pgo () { - local build="$1" - build_dir_is_usable "$build" || exit 1 - rm -fr "$build" - meson setup --buildtype=release -Db_pgo=generate "$build" || exit 1 - ninja -C "$build" || exit 1 - copy_directory_from_repo data "$build/src" - "$build/src/lite-xl" - meson configure -Db_pgo=use "$build" - ninja -C "$build" || exit 1 -} - -lite_build_package_windows () { - local portable="-msys" - if [ "$1" == "-portable" ]; then - portable="" - shift - fi - local build="$1" - local arch="$2" - local os="win" - local pdir=".package-build/lite-xl" - if [ -z "$portable" ]; then - local bindir="$pdir" - local datadir="$pdir/data" - else - local bindir="$pdir/bin" - local datadir="$pdir/share/lite-xl" - fi - mkdir -p "$bindir" - mkdir -p "$datadir" - for module_name in core plugins colors fonts; do - copy_directory_from_repo --strip-components=1 "data/$module_name" "$datadir" + for i in "$@"; do + case $i in + -h|--help) + show_help + exit 0 + ;; + -b|--builddir) + build_dir="$2" + shift + shift + ;; + -d|--destdir) + dest_dir="$2" + shift + shift + ;; + -f|--forcefallback) + force_fallback="--forcefallback" + shift + ;; + -p|--prefix) + prefix="$2" + shift + shift + ;; + -v|--version) + version="$2" + shift + shift + ;; + -A|--appimage) + appimage="--appimage" + shift + ;; + -B|--bundle) + bundle="--bundle" + shift + ;; + -D|--dmg) + dmg="--dmg" + shift + ;; + -I|--innosetup) + innosetup="--innosetup" + shift + ;; + -P|--portable) + portable="--portable" + shift + ;; + -S|--source) + source="--source" + shift + ;; + -O|--pgo) + pgo="--pgo" + shift + ;; + --debug) + debug="--debug" + set -x + shift + ;; + *) + # unknown option + ;; + esac done - for module_name in plugins colors; do - cp -r "$build/third/data/$module_name" "$datadir" - done - cp "$build/src/lite-xl.exe" "$bindir" - strip --strip-all "$bindir/lite-xl.exe" - pushd ".package-build" - local package_name="lite-xl-$os-$arch$portable.zip" - zip "$package_name" -r "lite-xl" - mv "$package_name" .. - popd - rm -fr ".package-build" - echo "created package $package_name" -} -lite_build_package_macos () { - local build="$1" - local arch="$2" - local os="macos" - - local appdir=".package-build/lite-xl.app" - local bindir="$appdir/Contents/MacOS" - local datadir="$appdir/Contents/Resources" - mkdir -p "$bindir" "$datadir" - for module_name in core plugins colors fonts; do - copy_directory_from_repo --strip-components=1 "data/$module_name" "$datadir" - done - for module_name in plugins colors; do - cp -r "$build/third/data/$module_name" "$datadir" - done - cp resources/icons/icon.icns "$appdir/Contents/Resources/icon.icns" - cp resources/macos/Info.plist "$appdir/Contents/Info.plist" - cp "$build/src/lite-xl" "$bindir/lite-xl" - strip "$bindir/lite-xl" - pushd ".package-build" - local package_name="lite-xl-$os-$arch.zip" - zip "$package_name" -r "lite-xl.app" - mv "$package_name" .. - popd - rm -fr ".package-build" - echo "created package $package_name" -} - -lite_build_package_linux () { - local portable="" - if [ "$1" == "-portable" ]; then - portable="-portable" - shift - fi - local build="$1" - local arch="$2" - local os="linux" - local pdir=".package-build/lite-xl" - if [ "$portable" == "-portable" ]; then - local bindir="$pdir" - local datadir="$pdir/data" - else - local bindir="$pdir/bin" - local datadir="$pdir/share/lite-xl" - fi - mkdir -p "$bindir" - mkdir -p "$datadir" - for module_name in core plugins colors fonts; do - copy_directory_from_repo --strip-components=1 "data/$module_name" "$datadir" - done - for module_name in plugins colors; do - cp -r "$build/third/data/$module_name" "$datadir" - done - cp "$build/src/lite-xl" "$bindir" - strip "$bindir/lite-xl" - if [ -z "$portable" ]; then - mkdir -p "$pdir/share/applications" "$pdir/share/icons/hicolor/scalable/apps" - cp "resources/linux/lite-xl.desktop" "$pdir/share/applications" - cp "resources/icons/lite-xl.svg" "$pdir/share/icons/hicolor/scalable/apps/lite-xl.svg" - fi - pushd ".package-build" - local package_name="lite-xl-$os-$arch$portable.tar.gz" - tar czf "$package_name" "lite-xl" - mv "$package_name" .. - popd - rm -fr ".package-build" - echo "created package $package_name" -} - -lite_build_package () { - if [[ "$OSTYPE" == msys || "$OSTYPE" == win32 ]]; then - lite_build_package_windows "$@" - elif [[ "$OSTYPE" == "darwin"* ]]; then - lite_build_package_macos "$@" - elif [[ "$OSTYPE" == "linux"* || "$OSTYPE" == "freebsd"* ]]; then - lite_build_package_linux "$@" - else - echo "Unknown OS type \"$OSTYPE\"" + if [[ -n $1 ]]; then + show_help exit 1 fi + + if [[ -n $build_dir ]]; then build_dir_option=("--builddir" "${build_dir}"); fi + if [[ -n $dest_dir ]]; then dest_dir_option=("--destdir" "${dest_dir}"); fi + if [[ -n $prefix ]]; then prefix_option=("--prefix" "${prefix}"); fi + if [[ -n $version ]]; then version_option=("--version" "${version}"); fi + + source scripts/build.sh \ + ${build_dir_option[@]} \ + ${prefix_option[@]} \ + $debug \ + $force_fallback \ + $bundle \ + $portable \ + $pgo + + source scripts/package.sh \ + ${build_dir_option[@]} \ + ${dest_dir_option[@]} \ + ${prefix_option[@]} \ + ${version_option[@]} \ + --binary \ + --addons \ + $debug \ + $appimage \ + $dmg \ + $innosetup \ + $source } -lite_copy_third_party_modules () { - local build="$1" - curl --insecure -L "https://github.com/rxi/lite-colors/archive/master.zip" -o "$build/rxi-lite-colors.zip" - mkdir -p "$build/third/data/colors" "$build/third/data/plugins" - unzip "$build/rxi-lite-colors.zip" -d "$build" - mv "$build/lite-colors-master/colors" "$build/third/data" - rm -fr "$build/lite-colors-master" -} - -unset arch -while [ ! -z {$1+x} ]; do - case $1 in - -pgo) - pgo=true - shift - ;; - -branch=*) - use_branch="${1#-branch=}" - shift - ;; - *) - arch="$1" - break - esac -done - -if [ -z ${arch+set} ]; then - echo "usage: $0 [options] " - exit 1 -fi - -if [ -z ${use_branch+set} ]; then - use_branch="$(git rev-parse --abbrev-ref HEAD)" -fi - -build_dir=".build-$arch" - -if [ -z ${pgo+set} ]; then - lite_build "$build_dir" -else - lite_build_pgo "$build_dir" -fi -lite_copy_third_party_modules "$build_dir" -lite_build_package "$build_dir" "$arch" -if [[ ! ( "$OSTYPE" == "linux"* || "$OSTYPE" == "freebsd"* || "$OSTYPE" == "darwin"* ) ]]; then - lite_build_package -portable "$build_dir" "$arch" -fi +main "$@" diff --git a/changelog.md b/changelog.md index eb203747..57ab9646 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,85 @@ This files document the changes done in Lite XL for each release. +### 2.0.2 + +Fix problem project directory when starting the application from Launcher on macOS. + +Improved LogView. Entries can now be expanded and there is a context menu to copy the item's content. + +Change the behavior of `ctrl+d` to add a multi-cursor selection to the next occurrence. +The old behavior to move the selection to the next occurrence is now done using the shortcut `ctrl+f3`. + +Added a command to create a multi-cursor with all the occurrences of the current selection. +Activated with the shortcut `ctrl+shift+l`. + +Fix problem when trying to close an unsaved new document. + +No longer shows an error for the `-psn` argument passed to the application on macOS. + +Fix `treeview:open-in-system` command on Windows. + +Fix rename command to update name of document if opened. + +Improve the find and replace dialog so that previously used expressions can be recalled +using "up" and "down" keys. + +Build package script rewrite with many improvements. + +Use bigger fonts by default. + +Other minor improvements and fixes. + +With many thanks to the contributors: @adamharrison, @takase1121, @Guldoman, @redtide, @Timofffee, @boppyt, @Jan200101. + +### 2.0.1 + +Fix a few bugs and we mandate the mod-version 2 for plugins. +This means that users should ensure they have up-to-date plugins for Lite XL 2.0. + +Here some details about the bug fixes: + +- fix a bug that created a fatal error when using the command to change project folder or when closing all the active documents +- add a limit to avoid scaling fonts too much and fix a related invalid memory access for very small fonts +- fix focus problem with NagView when switching project directory +- fix error that prevented the verification of plugins versions +- fix error on X11 that caused a bug window event on exit + +### 2.0 + +The 2.0 version of lite contains *breaking changes* to lite, in terms of how plugin settings are structured; +any custom plugins may need to be adjusted accordingly (see note below about plugin namespacing). + +Contains the following new features: + +Full PCRE (regex) support for find and replace, as well as in language syntax definitions. Can be accessed +programatically via the lua `regex` module. + +A full, finalized subprocess API, using libreproc. Subprocess can be started and interacted with using +`Process.new`. + +Support for multi-cursor editing. Cursors can be created by either ctrl+clicking on the screen, or by using +the keyboard shortcuts ctrl+shift+up/down to create an additional cursor on the previous/next line. + +All build systems other than meson removed. + +A more organized directory structure has been implemented; in particular a docs folder which contains C api +documentation, and a resource folder which houses all build resources. + +Plugin config namespacing has been implemented. This means that instead of using `config.myplugin.a`, +to read settings, and `config.myplugin = false` to disable plugins, this has been changed to +`config.plugins.myplugin.a`, and `config.plugins.myplugin = false` repsectively. This may require changes to +your user plugin, or to any custom plugins you have. + +A context menu on right click has been added. + +Changes to how we deal with indentation have been implemented; in particular, hitting home no longer brings you +to the start of a line, it'll bring you to the start of indentation, which is more in line with other editors. + +Lineguide, and scale plugins moved into the core, and removed from `lite-plugins`. This may also require you to +adjust your personal plugin folder to remove these if they're present. + +In addition, there have been many other small fixes and improvements, too numerous to list here. + ### 1.16.11 When opening directories with too many files lite-xl now keep diplaying files and directories in the treeview. diff --git a/data/colors/textadept.lua b/data/colors/textadept.lua new file mode 100644 index 00000000..276406d9 --- /dev/null +++ b/data/colors/textadept.lua @@ -0,0 +1,51 @@ +local b05 = 'rgba(0,0,0,0.5)' local red = '#994D4D' +local b80 = '#333333' local orange = '#B3661A' +local b60 = '#808080' local green = '#52994D' +local b40 = '#ADADAD' local teal = '#4D9999' +local b20 = '#CECECE' local blue = '#1A66B3' +local b00 = '#E6E6E6' local magenta = '#994D99' +--------------------------=-------------------------- +local style = require 'core.style' +local common = require 'core.common' +--------------------------=-------------------------- +style.line_highlight = { common.color(b20) } +style.background = { common.color(b00) } +style.background2 = { common.color(b20) } +style.background3 = { common.color(b20) } +style.text = { common.color(b60) } +style.caret = { common.color(b80) } +style.accent = { common.color(b80) } +style.dim = { common.color(b60) } +style.divider = { common.color(b40) } +style.selection = { common.color(b40) } +style.line_number = { common.color(b60) } +style.line_number2 = { common.color(b80) } +style.scrollbar = { common.color(b40) } +style.scrollbar2 = { common.color(b60) } +style.nagbar = { common.color(red) } +style.nagbar_text = { common.color(b00) } +style.nagbar_dim = { common.color(b05) } +--------------------------=-------------------------- +style.syntax = {} +style.syntax['normal'] = { common.color(b80) } +style.syntax['symbol'] = { common.color(b80) } +style.syntax['comment'] = { common.color(b60) } +style.syntax['keyword'] = { common.color(blue) } +style.syntax['keyword2'] = { common.color(red) } +style.syntax['number'] = { common.color(teal) } +style.syntax['literal'] = { common.color(blue) } +style.syntax['string'] = { common.color(green) } +style.syntax['operator'] = { common.color(magenta) } +style.syntax['function'] = { common.color(blue) } +--------------------------=-------------------------- +style.syntax.paren1 = { common.color(magenta) } +style.syntax.paren2 = { common.color(orange) } +style.syntax.paren3 = { common.color(teal) } +style.syntax.paren4 = { common.color(blue) } +style.syntax.paren5 = { common.color(red) } +--------------------------=-------------------------- +style.lint = {} +style.lint.info = { common.color(blue) } +style.lint.hint = { common.color(green) } +style.lint.warning = { common.color(red) } +style.lint.error = { common.color(orange) } diff --git a/data/core/command.lua b/data/core/command.lua index 7915e16d..2531fb96 100644 --- a/data/core/command.lua +++ b/data/core/command.lua @@ -42,10 +42,10 @@ function command.get_all_valid() end -local function perform(name) +local function perform(name, ...) local cmd = command.map[name] - if cmd and cmd.predicate() then - cmd.perform() + if cmd and cmd.predicate(...) then + cmd.perform(...) return true end return false diff --git a/data/core/commands/core.lua b/data/core/commands/core.lua index 859fb066..432ded89 100644 --- a/data/core/commands/core.lua +++ b/data/core/commands/core.lua @@ -104,11 +104,20 @@ command.add(nil, { end, function (text) return common.home_encode_list(common.path_suggest(common.home_expand(text))) end, nil, function(text) - local path_stat, err = system.get_file_info(common.home_expand(text)) + local filename = common.home_expand(text) + local path_stat, err = system.get_file_info(filename) if err then - core.error("Cannot open file %q: %q", text, err) + if err:find("No such file", 1, true) then + -- check if the containing directory exists + local dirname = common.dirname(filename) + local dir_stat = dirname and system.get_file_info(dirname) + if not dirname or (dir_stat and dir_stat.type == 'dir') then + return true + end + end + core.error("Cannot open file %s: %s", text, err) elseif path_stat.type == 'dir' then - core.error("Cannot open %q, is a folder", text) + core.error("Cannot open %s, is a folder", text) else return true end @@ -138,6 +147,10 @@ command.add(nil, { end, ["core:change-project-folder"] = function() + local dirname = common.dirname(core.project_dir) + if dirname then + core.command_view:set_text(common.home_encode(dirname) .. PATHSEP) + end core.command_view:enter("Change Project Folder", function(text, item) text = system.absolute_path(common.home_expand(item and item.text or text)) if text == core.project_dir then return end @@ -146,11 +159,15 @@ command.add(nil, { core.error("Cannot open folder %q", text) return end - core.confirm_close_all(core.open_folder_project, text) + core.confirm_close_docs(core.docs, core.open_folder_project, text) end, suggest_directory) end, ["core:open-project-folder"] = function() + local dirname = common.dirname(core.project_dir) + if dirname then + core.command_view:set_text(common.home_encode(dirname) .. PATHSEP) + end core.command_view:enter("Open Project", function(text, item) text = common.home_expand(item and item.text or text) local path_stat = system.get_file_info(text) diff --git a/data/core/commands/doc.lua b/data/core/commands/doc.lua index ca39bfde..ba3d1f0c 100644 --- a/data/core/commands/doc.lua +++ b/data/core/commands/doc.lua @@ -43,7 +43,12 @@ local function append_line_if_last_line(line) end local function save(filename) - doc():save(filename and core.normalize_to_project_dir(filename)) + local abs_filename + if filename then + filename = core.normalize_to_project_dir(filename) + abs_filename = core.project_absolute_path(filename) + end + doc():save(filename, abs_filename) local saved_filename = doc().filename core.log("Saved \"%s\"", saved_filename) end @@ -62,13 +67,14 @@ local function cut_or_copy(delete) doc().cursor_clipboard[idx] = "" end end + doc().cursor_clipboard["full"] = full_text system.set_clipboard(full_text) end local function split_cursor(direction) local new_cursors = {} for _, line1, col1 in doc():get_selections() do - if line1 > 1 and line1 < #doc().lines then + if line1 + direction >= 1 and line1 + direction <= #doc().lines then table.insert(new_cursors, { line1 + direction, col1 }) end end @@ -76,6 +82,16 @@ local function split_cursor(direction) core.blink_reset() end +local function set_cursor(x, y, type) + local line, col = dv():resolve_screen_position(x, y) + doc():set_selection(line, col, line, col) + if type == "word" or type == "lines" then + command.perform("doc:select-" .. type) + end + dv().mouse_selecting = { line, col } + core.blink_reset() +end + local commands = { ["doc:undo"] = function() doc():undo() @@ -94,8 +110,13 @@ local commands = { end, ["doc:paste"] = function() + local clipboard = system.get_clipboard() + -- If the clipboard has changed since our last look, use that instead + if doc().cursor_clipboard["full"] ~= clipboard then + doc().cursor_clipboard = {} + end for idx, line1, col1, line2, col2 in doc():get_selections() do - local value = doc().cursor_clipboard[idx] or system.get_clipboard() + local value = doc().cursor_clipboard[idx] or clipboard doc():text_input(value:gsub("\r", ""), idx) end end, @@ -157,16 +178,6 @@ local commands = { doc():set_selection(line, col) end, - - ["doc:indent"] = function() - for idx, line1, col1, line2, col2 in doc_multiline_selections(true) do - local l1, c1, l2, c2 = doc():indent_text(false, line1, col1, line2, col2) - if l1 then - doc():set_selections(idx, l1, c1, l2, c2) - end - end - end, - ["doc:select-lines"] = function() for idx, line1, _, line2 in doc():get_selections(true) do append_line_if_last_line(line2) @@ -363,12 +374,14 @@ local commands = { end core.command_view:set_text(old_filename) core.command_view:enter("Rename", function(filename) - doc():save(filename) + save(common.home_expand(filename)) core.log("Renamed \"%s\" to \"%s\"", old_filename, filename) if filename ~= old_filename then os.remove(old_filename) end - end, common.path_suggest) + end, function (text) + return common.home_encode_list(common.path_suggest(common.home_expand(text))) + end) end, @@ -385,6 +398,30 @@ local commands = { os.remove(filename) core.log("Removed \"%s\"", filename) end, + + ["doc:select-to-cursor"] = function(x, y, clicks) + local line1, col1 = select(3, doc():get_selection()) + local line2, col2 = dv():resolve_screen_position(x, y) + dv().mouse_selecting = { line1, col1 } + doc():set_selection(line2, col2, line1, col1) + end, + + ["doc:set-cursor"] = function(x, y) + set_cursor(x, y, "set") + end, + + ["doc:set-cursor-word"] = function(x, y) + set_cursor(x, y, "word") + end, + + ["doc:set-cursor-line"] = function(x, y, clicks) + set_cursor(x, y, "lines") + end, + + ["doc:split-cursor"] = function(x, y, clicks) + local line, col = dv():resolve_screen_position(x, y) + doc():add_selection(line, col, line, col) + end, ["doc:create-cursor-previous-line"] = function() split_cursor(-1) @@ -411,6 +448,7 @@ local translations = { ["start-of-line"] = translate.start_of_line, ["end-of-line"] = translate.end_of_line, ["start-of-word"] = translate.start_of_word, + ["start-of-indentation"] = translate.start_of_indentation, ["end-of-word"] = translate.end_of_word, ["previous-line"] = DocView.translate.previous_line, ["next-line"] = DocView.translate.next_line, diff --git a/data/core/commands/findreplace.lua b/data/core/commands/findreplace.lua index 7904632a..d1af0d88 100644 --- a/data/core/commands/findreplace.lua +++ b/data/core/commands/findreplace.lua @@ -12,6 +12,7 @@ local last_finds, last_view, last_fn, last_text, last_sel local case_sensitive = config.find_case_sensitive or false local find_regex = config.find_regex or false +local found_expression local function doc() return core.active_view:is(DocView) and core.active_view.doc or last_view.doc @@ -29,43 +30,57 @@ local function get_find_tooltip() end local function update_preview(sel, search_fn, text) - local ok, line1, col1, line2, col2 = - pcall(search_fn, last_view.doc, sel[1], sel[2], text, case_sensitive, find_regex) + local ok, line1, col1, line2, col2 = pcall(search_fn, last_view.doc, + sel[1], sel[2], text, case_sensitive, find_regex) if ok and line1 and text ~= "" then last_view.doc:set_selection(line2, col2, line1, col1) last_view:scroll_to_line(line2, true) - return true + found_expression = true else - last_view.doc:set_selection(unpack(sel)) - return false + last_view.doc:set_selection(table.unpack(sel)) + found_expression = false end end + +local function insert_unique(t, v) + local n = #t + for i = 1, n do + if t[i] == v then return end + end + t[n + 1] = v +end + + local function find(label, search_fn) last_view, last_sel, last_finds = core.active_view, { core.active_view.doc:get_selection() }, {} - local text, found = last_view.doc:get_text(unpack(last_sel)), false + local text = last_view.doc:get_text(table.unpack(last_sel)) + found_expression = false core.command_view:set_text(text, true) core.status_view:show_tooltip(get_find_tooltip()) - core.command_view:enter(label, function(text) + core.command_view:set_hidden_suggestions() + core.command_view:enter(label, function(text, item) + insert_unique(core.previous_find, text) core.status_view:remove_tooltip() - if found then + if found_expression then last_fn, last_text = search_fn, text else core.error("Couldn't find %q", text) - last_view.doc:set_selection(unpack(last_sel)) - last_view:scroll_to_make_visible(unpack(last_sel)) + last_view.doc:set_selection(table.unpack(last_sel)) + last_view:scroll_to_make_visible(table.unpack(last_sel)) end end, function(text) - found = update_preview(last_sel, search_fn, text) + update_preview(last_sel, search_fn, text) last_fn, last_text = search_fn, text + return core.previous_find end, function(explicit) core.status_view:remove_tooltip() if explicit then - last_view.doc:set_selection(unpack(last_sel)) - last_view:scroll_to_make_visible(unpack(last_sel)) + last_view.doc:set_selection(table.unpack(last_sel)) + last_view:scroll_to_make_visible(table.unpack(last_sel)) end end) end @@ -75,18 +90,25 @@ local function replace(kind, default, fn) core.command_view:set_text(default, true) core.status_view:show_tooltip(get_find_tooltip()) + core.command_view:set_hidden_suggestions() core.command_view:enter("Find To Replace " .. kind, function(old) + insert_unique(core.previous_find, old) core.command_view:set_text(old, true) local s = string.format("Replace %s %q With", kind, old) + core.command_view:set_hidden_suggestions() core.command_view:enter(s, function(new) + core.status_view:remove_tooltip() + insert_unique(core.previous_replace, new) local n = doc():replace(function(text) return fn(text, old, new) end) core.log("Replaced %d instance(s) of %s %q with %q", n, kind, old, new) - end, function() end, function() + end, function() return core.previous_replace end, function() core.status_view:remove_tooltip() end) + end, function() return core.previous_find end, function() + core.status_view:remove_tooltip() end) end @@ -94,13 +116,60 @@ local function has_selection() return core.active_view:is(DocView) and core.active_view.doc:has_selection() end -command.add(has_selection, { +local function has_unique_selection() + if not core.active_view:is(DocView) then return false end + local text = nil + for idx, line1, col1, line2, col2 in doc():get_selections(true, true) do + if line1 == line2 and col1 == col2 then return false end + local selection = doc():get_text(line1, col1, line2, col2) + if text ~= nil and text ~= selection then return false end + text = selection + end + return text ~= nil +end + +local function is_in_selection(line, col, l1, c1, l2, c2) + if line < l1 or line > l2 then return false end + if line == l1 and col <= c1 then return false end + if line == l2 and col > c2 then return false end + return true +end + +local function is_in_any_selection(line, col) + for idx, l1, c1, l2, c2 in doc():get_selections(true, false) do + if is_in_selection(line, col, l1, c1, l2, c2) then return true end + end + return false +end + +local function select_next(all) + local il1, ic1 = doc():get_selection(true) + for idx, l1, c1, l2, c2 in doc():get_selections(true, true) do + local text = doc():get_text(l1, c1, l2, c2) + repeat + l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true }) + if l1 == il1 and c1 == ic1 then break end + if l2 and (all or not is_in_any_selection(l2, c2)) then + doc():add_selection(l2, c2, l1, c1) + if not all then + core.active_view:scroll_to_make_visible(l2, c2) + return + end + end + until not all or not l2 + if all then break end + end +end + +command.add(has_unique_selection, { ["find-replace:select-next"] = function() local l1, c1, l2, c2 = doc():get_selection(true) local text = doc():get_text(l1, c1, l2, c2) l1, c1, l2, c2 = search.find(doc(), l2, c2, text, { wrap = true }) if l2 then doc():set_selection(l2, c2, l1, c1) end - end + end, + ["find-replace:select-add-next"] = function() select_next(false) end, + ["find-replace:select-add-all"] = function() select_next(true) end }) command.add("core.docview", { @@ -112,11 +181,13 @@ command.add("core.docview", { end, ["find-replace:replace"] = function() - replace("Text", doc():get_text(doc():get_selection(true)), function(text, old, new) + local l1, c1, l2, c2 = doc():get_selection() + local selected_text = doc():get_text(l1, c1, l2, c2) + replace("Text", l1 == l2 and selected_text or "", function(text, old, new) if not find_regex then return text:gsub(old:gsub("%W", "%%%1"), new:gsub("%%", "%%%%"), nil) end - local result, matches = regex.gsub(regex.compile(old), text, new) + local result, matches = regex.gsub(regex.compile(old, "m"), text, new) return result, #matches end) end, @@ -180,12 +251,12 @@ command.add("core.commandview", { ["find-replace:toggle-sensitivity"] = function() case_sensitive = not case_sensitive core.status_view:show_tooltip(get_find_tooltip()) - update_preview(last_sel, last_fn, last_text) + if last_sel then update_preview(last_sel, last_fn, last_text) end end, ["find-replace:toggle-regex"] = function() find_regex = not find_regex core.status_view:show_tooltip(get_find_tooltip()) - update_preview(last_sel, last_fn, last_text) + if last_sel then update_preview(last_sel, last_fn, last_text) end end }) diff --git a/data/core/commands/root.lua b/data/core/commands/root.lua index 7bc13283..8f2536b8 100644 --- a/data/core/commands/root.lua +++ b/data/core/commands/root.lua @@ -3,6 +3,7 @@ local style = require "core.style" local DocView = require "core.docview" local command = require "core.command" local common = require "core.common" +local config = require "core.config" local t = { @@ -21,9 +22,15 @@ local t = { end, ["root:close-all"] = function() - core.confirm_close_all(core.root_view.close_all_docviews, core.root_view) + core.confirm_close_docs(core.docs, core.root_view.close_all_docviews, core.root_view) end, + ["root:close-all-others"] = function() + local active_doc, docs = core.active_view and core.active_view.doc, {} + for i, v in ipairs(core.docs) do if v ~= active_doc then table.insert(docs, v) end end + core.confirm_close_docs(docs, core.root_view.close_all_docviews, core.root_view, true) + end, + ["root:switch-to-previous-tab"] = function() local node = core.root_view:get_active_node() local idx = node:get_view_idx(core.active_view) @@ -57,7 +64,7 @@ local t = { table.insert(node.views, idx + 1, core.active_view) end end, - + ["root:shrink"] = function() local node = core.root_view:get_active_node() local parent = node:get_parent_node(core.root_view.root_node) @@ -70,7 +77,7 @@ local t = { local parent = node:get_parent_node(core.root_view.root_node) local n = (parent.a == node) and 0.1 or -0.1 parent.divider = common.clamp(parent.divider + n, 0.1, 0.9) - end, + end } @@ -116,3 +123,14 @@ command.add(function() local node = core.root_view:get_active_node() return not node:get_locked_size() end, t) + +command.add(nil, { + ["root:scroll"] = function(delta) + local view = (core.root_view.overlapping_node and core.root_view.overlapping_node.active_view) or core.active_view + if view and view.scrollable then + view.scroll.to.y = view.scroll.to.y + delta * -config.mouse_wheel_scroll + return true + end + return false + end +}) diff --git a/data/core/commandview.lua b/data/core/commandview.lua index 4d518d02..b91f1394 100644 --- a/data/core/commandview.lua +++ b/data/core/commandview.lua @@ -15,6 +15,8 @@ end local CommandView = DocView:extend() +CommandView.context = "application" + local max_suggestions = 10 local noop = function() end @@ -32,6 +34,7 @@ function CommandView:new() self.suggestion_idx = 1 self.suggestions = {} self.suggestions_height = 0 + self.show_suggestions = true self.last_change_id = 0 self.gutter_width = 0 self.gutter_text_brightness = 0 @@ -43,6 +46,11 @@ function CommandView:new() end +function CommandView:set_hidden_suggestions() + self.show_suggestions = false +end + + function CommandView:get_name() return View.get_name(self) end @@ -81,10 +89,29 @@ end function CommandView:move_suggestion_idx(dir) - local n = self.suggestion_idx + dir - self.suggestion_idx = common.clamp(n, 1, #self.suggestions) - self:complete() - self.last_change_id = self.doc:get_change_id() + if self.show_suggestions then + local n = self.suggestion_idx + dir + self.suggestion_idx = common.clamp(n, 1, #self.suggestions) + self:complete() + self.last_change_id = self.doc:get_change_id() + else + local current_suggestion = #self.suggestions > 0 and self.suggestions[self.suggestion_idx].text + local text = self:get_text() + if text == current_suggestion then + local n = self.suggestion_idx + dir + if n == 0 and self.save_suggestion then + self:set_text(self.save_suggestion) + else + self.suggestion_idx = common.clamp(n, 1, #self.suggestions) + self:complete() + end + else + self.save_suggestion = text + self:complete() + end + self.last_change_id = self.doc:get_change_id() + self.state.suggest(self:get_text()) + end end @@ -132,6 +159,8 @@ function CommandView:exit(submitted, inexplicit) self.doc:reset() self.suggestions = {} if not submitted then cancel(not inexplicit) end + self.show_suggestions = true + self.save_suggestion = nil end @@ -185,7 +214,7 @@ function CommandView:update() -- update suggestions box height local lh = self:get_suggestion_line_height() - local dest = math.min(#self.suggestions, max_suggestions) * lh + local dest = self.show_suggestions and math.min(#self.suggestions, max_suggestions) * lh or 0 self:move_towards("suggestions_height", dest) -- update suggestion cursor offset @@ -254,7 +283,9 @@ end function CommandView:draw() CommandView.super.draw(self) - core.root_view:defer_draw(draw_suggestions_box, self) + if self.show_suggestions then + core.root_view:defer_draw(draw_suggestions_box, self) + end end diff --git a/data/core/common.lua b/data/core/common.lua index bd6d52e7..1a1b22cd 100644 --- a/data/core/common.lua +++ b/data/core/common.lua @@ -41,6 +41,11 @@ function common.lerp(a, b, t) end +function common.distance(x1, y1, x2, y2) + return math.sqrt(math.pow(x2-x1, 2)+math.pow(y2-y1, 2)) +end + + function common.color(str) local r, g, b, a = str:match("#(%x%x)(%x%x)(%x%x)") if r then @@ -230,6 +235,12 @@ function common.basename(path) end +-- can return nil if there is no directory part in the path +function common.dirname(path) + return path:match("(.+)[\\/][^\\/]+$") +end + + function common.home_encode(text) if HOME and string.find(text, HOME, 1, true) == 1 then local dir_pos = #HOME + 1 @@ -257,16 +268,6 @@ function common.home_expand(text) end -function common.normalize_path(filename) - if PATHSEP == '\\' then - filename = filename:gsub('[/\\]', '\\') - local drive, rem = filename:match('^([a-zA-Z])(:.*)') - return drive and drive:upper() .. rem or filename - end - return filename -end - - local function split_on_slash(s, sep_pattern) local t = {} if s:match("^[/\\]") then @@ -279,8 +280,29 @@ local function split_on_slash(s, sep_pattern) end +function common.normalize_path(filename) + if not filename then return end + if PATHSEP == '\\' then + filename = filename:gsub('[/\\]', '\\') + local drive, rem = filename:match('^([a-zA-Z])(:.*)') + filename = drive and drive:upper() .. rem or filename + end + local parts = split_on_slash(filename, PATHSEP) + local accu = {} + for _, part in ipairs(parts) do + if part == '..' and #accu > 0 and accu[#accu] ~= ".." then + table.remove(accu) + elseif part ~= '.' then + table.insert(accu, part) + end + end + local npath = table.concat(accu, PATHSEP) + return npath == "" and PATHSEP or npath +end + + function common.path_belongs_to(filename, path) - return filename and string.find(filename, path .. PATHSEP, 1, true) == 1 + return string.find(filename, path .. PATHSEP, 1, true) == 1 end diff --git a/data/core/config.lua b/data/core/config.lua index 401ac0af..faffc27e 100644 --- a/data/core/config.lua +++ b/data/core/config.lua @@ -5,6 +5,7 @@ config.fps = 60 config.max_log_items = 80 config.message_timeout = 5 config.mouse_wheel_scroll = 50 * SCALE +config.scroll_past_end = true config.file_size_limit = 10 config.ignore_files = "^%." config.symbol_pattern = "[%a_][%w_]*" @@ -12,6 +13,7 @@ config.non_word_chars = " \t\n/\\()\"':,.;<>~!@#$%^&*|+=[]{}`?-" config.undo_merge_timeout = 0.3 config.max_undos = 10000 config.max_tabs = 10 +config.always_show_tabs = false config.highlight_current_line = true config.line_height = 1.2 config.indent_size = 2 @@ -22,9 +24,11 @@ config.max_project_files = 2000 config.transitions = true config.animation_rate = 1.0 config.blink_period = 0.8 +config.disable_blink = false config.draw_whitespace = false config.borderless = false config.tab_close_button = true +config.max_clicks = 3 -- Disable plugin loading setting to false the config entry -- of the same name. @@ -32,5 +36,6 @@ config.plugins = {} config.plugins.trimwhitespace = false config.plugins.lineguide = false +config.plugins.drawwhitespace = false return config diff --git a/data/core/contextmenu.lua b/data/core/contextmenu.lua index 36247597..d6131cdf 100644 --- a/data/core/contextmenu.lua +++ b/data/core/contextmenu.lua @@ -49,7 +49,7 @@ function ContextMenu:register(predicate, items) local width, height = 0, 0 --precalculate the size of context menu for i, item in ipairs(items) do if item ~= DIVIDER then - item.info = keymap.reverse_map[item.command] + item.info = keymap.get_binding(item.command) end local lw, lh = get_item_size(item) width = math.max(width, lw) diff --git a/data/core/doc/highlighter.lua b/data/core/doc/highlighter.lua index e7650d01..4cb703da 100644 --- a/data/core/doc/highlighter.lua +++ b/data/core/doc/highlighter.lua @@ -24,7 +24,7 @@ function Highlighter:new(doc) for i = self.first_invalid_line, max do local state = (i > 1) and self.lines[i - 1].state local line = self.lines[i] - if not (line and line.init_state == state) then + if not (line and line.init_state == state and line.text == self.doc.lines[i]) then self.lines[i] = self:tokenize_line(i, state) end end @@ -44,12 +44,25 @@ function Highlighter:reset() self.max_wanted_line = 0 end - function Highlighter:invalidate(idx) self.first_invalid_line = math.min(self.first_invalid_line, idx) self.max_wanted_line = math.min(self.max_wanted_line, #self.doc.lines) end +function Highlighter:insert_notify(line, n) + self:invalidate(line) + for i = 1, n do + table.insert(self.lines, line, nil) + end +end + +function Highlighter:remove_notify(line, n) + self:invalidate(line) + for i = 1, n do + table.remove(self.lines, line) + end +end + function Highlighter:tokenize_line(idx, state) local res = {} diff --git a/data/core/doc/init.lua b/data/core/doc/init.lua index c33ade5a..640e9fd5 100644 --- a/data/core/doc/init.lua +++ b/data/core/doc/init.lua @@ -17,10 +17,15 @@ local function split_lines(text) return res end -function Doc:new(filename) + +function Doc:new(filename, abs_filename, new_file) + self.new_file = new_file self:reset() if filename then - self:load(filename) + self:set_filename(filename, abs_filename) + if not new_file then + self:load(filename) + end end end @@ -47,16 +52,15 @@ function Doc:reset_syntax() end -function Doc:set_filename(filename) +function Doc:set_filename(filename, abs_filename) self.filename = filename - self.abs_filename = system.absolute_path(filename) + self.abs_filename = abs_filename end function Doc:load(filename) local fp = assert( io.open(filename, "rb") ) self:reset() - self:set_filename(filename) self.lines = {} for line in fp:lines() do if line:byte(-1) == 13 then @@ -73,17 +77,20 @@ function Doc:load(filename) end -function Doc:save(filename) - filename = filename or assert(self.filename, "no filename set to default to") +function Doc:save(filename, abs_filename) + if not filename then + assert(self.filename, "no filename set to default to") + filename = self.filename + abs_filename = self.abs_filename + end local fp = assert( io.open(filename, "wb") ) for _, line in ipairs(self.lines) do if self.crlf then line = line:gsub("\n", "\r\n") end fp:write(line) end fp:close() - if filename then - self:set_filename(filename) - end + self:set_filename(filename, abs_filename) + self.new_file = false self:reset_syntax() self:clean() end @@ -95,7 +102,11 @@ end function Doc:is_dirty() - return self.clean_change_id ~= self:get_change_id() + if self.new_file then + return #self.lines > 1 or #self.lines[1] > 1 + else + return self.clean_change_id ~= self:get_change_id() + end end @@ -117,6 +128,19 @@ function Doc:get_selection(sort) return line1, col1, line2, col2, sort end +function Doc:get_selection_text(limit) + limit = limit or math.huge + local result = {} + for idx, line1, col1, line2, col2 in self:get_selections() do + if idx > limit then break end + if line1 ~= line2 or col1 ~= col2 then + local text = self:get_text(line1, col1, line2, col2) + if text ~= "" then result[#result + 1] = text end + end + end + return table.concat(result, "\n") +end + function Doc:has_selection() local line1, col1, line2, col2 = self:get_selection(false) return line1 ~= line2 or col1 ~= col2 @@ -166,19 +190,21 @@ function Doc:merge_cursors(idx) if self.selections[i] == self.selections[j] and self.selections[i+1] == self.selections[j+1] then common.splice(self.selections, i, 4) + common.splice(self.cursor_clipboard, i, 1) break end end end + if #self.selections <= 4 then self.cursor_clipboard = {} end end local function selection_iterator(invariant, idx) local target = invariant[3] and (idx*4 - 7) or (idx*4 + 1) if target > #invariant[1] or target <= 0 or (type(invariant[3]) == "number" and invariant[3] ~= idx - 1) then return end if invariant[2] then - return idx+(invariant[3] and -1 or 1), sort_positions(unpack(invariant[1], target, target+4)) + return idx+(invariant[3] and -1 or 1), sort_positions(table.unpack(invariant[1], target, target+4)) else - return idx+(invariant[3] and -1 or 1), unpack(invariant[1], target, target+4) + return idx+(invariant[3] and -1 or 1), table.unpack(invariant[1], target, target+4) end end @@ -279,7 +305,7 @@ local function pop_undo(self, undo_stack, redo_stack, modified) local line1, col1, line2, col2 = table.unpack(cmd) self:raw_remove(line1, col1, line2, col2, redo_stack, cmd.time) elseif cmd.type == "selection" then - self.selections = { unpack(cmd) } + self.selections = { table.unpack(cmd) } end modified = modified or (cmd.type ~= "selection") @@ -300,6 +326,7 @@ end function Doc:raw_insert(line, col, text, undo_stack, time) -- split text into lines and merge with line at insertion point local lines = split_lines(text) + local len = #lines[#lines] local before = self.lines[line]:sub(1, col - 1) local after = self.lines[line]:sub(col) for i = 1, #lines - 1 do @@ -310,14 +337,22 @@ function Doc:raw_insert(line, col, text, undo_stack, time) -- splice lines into line array common.splice(self.lines, line, 1, lines) + + -- keep cursors where they should be + for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do + if cline1 < line then break end + local line_addition = (line < cline1 or col < ccol1) and #lines - 1 or 0 + local column_addition = line == cline1 and ccol1 > col and len or 0 + self:set_selections(idx, cline1 + line_addition, ccol1 + column_addition, cline2 + line_addition, ccol2 + column_addition) + end -- push undo local line2, col2 = self:position_offset(line, col, #text) - push_undo(undo_stack, time, "selection", unpack(self.selections)) + push_undo(undo_stack, time, "selection", table.unpack(self.selections)) push_undo(undo_stack, time, "remove", line, col, line2, col2) -- update highlighter and assure selection is in bounds - self.highlighter:invalidate(line) + self.highlighter:insert_notify(line, #lines - 1) self:sanitize_selection() end @@ -325,7 +360,7 @@ end function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time) -- push undo local text = self:get_text(line1, col1, line2, col2) - push_undo(undo_stack, time, "selection", unpack(self.selections)) + push_undo(undo_stack, time, "selection", table.unpack(self.selections)) push_undo(undo_stack, time, "insert", line1, col1, text) -- get line content before/after removed text @@ -334,9 +369,17 @@ function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time) -- splice line into line array common.splice(self.lines, line1, line2 - line1 + 1, { before .. after }) + + -- move all cursors back if they share a line with the removed text + for idx, cline1, ccol1, cline2, ccol2 in self:get_selections(true, true) do + if cline1 < line2 then break end + local line_removal = line2 - line1 + local column_removal = line2 == cline2 and col2 < ccol1 and (line2 == line1 and col2 - col1 or col2) or 0 + self:set_selections(idx, cline1 - line_removal, ccol1 - column_removal, cline2 - line_removal, ccol2 - column_removal) + end -- update highlighter and assure selection is in bounds - self.highlighter:invalidate(line1) + self.highlighter:remove_notify(line1, line2 - line1) self:sanitize_selection() end @@ -370,7 +413,7 @@ end function Doc:text_input(text, idx) - for sidx, line1, col1, line2, col2 in self:get_selections(true, idx) do + for sidx, line1, col1, line2, col2 in self:get_selections(true, idx or true) do if line1 ~= line2 or col1 ~= col2 then self:delete_to_cursor(sidx) end @@ -379,12 +422,7 @@ function Doc:text_input(text, idx) end end - -function Doc:replace(fn) - local line1, col1, line2, col2 = self:get_selection(true) - if line1 == line2 and col1 == col2 then - line1, col1, line2, col2 = 1, 1, #self.lines, #self.lines[#self.lines] - end +function Doc:replace_cursor(idx, line1, col1, line2, col2, fn) local old_text = self:get_text(line1, col1, line2, col2) local new_text, n = fn(old_text) if old_text ~= new_text then @@ -392,12 +430,27 @@ function Doc:replace(fn) self:remove(line1, col1, line2, col2) if line1 == line2 and col1 == col2 then line2, col2 = self:position_offset(line1, col1, #new_text) - self:set_selection(line1, col1, line2, col2) + self:set_selections(idx, line1, col1, line2, col2) end end return n end +function Doc:replace(fn) + local has_selection, n = false, 0 + for idx, line1, col1, line2, col2 in self:get_selections(true) do + if line1 ~= line2 or col1 ~= col2 then + n = n + self:replace_cursor(idx, line1, col1, line2, col2, fn) + has_selection = true + end + end + if not has_selection then + self:set_selection(table.unpack(self.selections)) + n = n + self:replace_cursor(1, 1, 1, #self.lines, #self.lines[#self.lines], fn) + end + return n +end + function Doc:delete_to_cursor(idx, ...) for sidx, line1, col1, line2, col2 in self:get_selections(true, idx) do diff --git a/data/core/doc/translate.lua b/data/core/doc/translate.lua index b084e89a..d1bde5f0 100644 --- a/data/core/doc/translate.lua +++ b/data/core/doc/translate.lua @@ -117,6 +117,10 @@ function translate.start_of_line(doc, line, col) return line, 1 end +function translate.start_of_indentation(doc, line, col) + local s, e = doc.lines[line]:find("^%s*") + return line, col > e + 1 and e + 1 or 1 +end function translate.end_of_line(doc, line, col) return line, math.huge diff --git a/data/core/docview.lua b/data/core/docview.lua index 89da8190..07da1cef 100644 --- a/data/core/docview.lua +++ b/data/core/docview.lua @@ -9,6 +9,7 @@ local View = require "core.view" local DocView = View:extend() +DocView.context = "session" local function move_to_line_offset(dv, line, col, offset) local xo = dv.last_x_offset @@ -97,6 +98,9 @@ end function DocView:get_scrollable_size() + if not config.scroll_past_end then + return self:get_line_height() * (#self.doc.lines) + style.padding.y * 2 + end return self:get_line_height() * (#self.doc.lines - 1) + self.size.y end @@ -149,14 +153,14 @@ function DocView:get_col_x_offset(line, col) local font = style.syntax_fonts[type] or default_font for char in common.utf8_chars(text) do if column == col then - return xoffset / font:subpixel_scale() + return xoffset end - xoffset = xoffset + font:get_width_subpixel(char) + xoffset = xoffset + font:get_width(char) column = column + #char end end - return xoffset / default_font:subpixel_scale() + return xoffset end @@ -165,14 +169,12 @@ function DocView:get_x_offset_col(line, x) local xoffset, last_i, i = 0, 1, 1 local default_font = self:get_font() - local subpixel_scale = default_font:subpixel_scale() - local x_subpixel = subpixel_scale * x + subpixel_scale / 2 for _, type, text in self.doc.highlighter:each_token(line) do local font = style.syntax_fonts[type] or default_font for char in common.utf8_chars(text) do - local w = font:get_width_subpixel(char) - if xoffset >= subpixel_scale * x then - return (xoffset - x_subpixel > w / 2) and last_i or i + local w = font:get_width(char) + if xoffset >= x then + return (xoffset - x > w / 2) and last_i or i end xoffset = xoffset + w last_i = i @@ -222,52 +224,6 @@ function DocView:scroll_to_make_visible(line, col) end end - -local function mouse_selection(doc, clicks, line1, col1, line2, col2) - local swap = line2 < line1 or line2 == line1 and col2 <= col1 - if swap then - line1, col1, line2, col2 = line2, col2, line1, col1 - end - if clicks % 4 == 2 then - line1, col1 = translate.start_of_word(doc, line1, col1) - line2, col2 = translate.end_of_word(doc, line2, col2) - elseif clicks % 4 == 3 then - if line2 == #doc.lines and doc.lines[#doc.lines] ~= "\n" then - doc:insert(math.huge, math.huge, "\n") - end - line1, col1, line2, col2 = line1, 1, line2 + 1, 1 - end - if swap then - return line2, col2, line1, col1 - end - return line1, col1, line2, col2 -end - - -function DocView:on_mouse_pressed(button, x, y, clicks) - local caught = DocView.super.on_mouse_pressed(self, button, x, y, clicks) - if caught then - return - end - if keymap.modkeys["shift"] then - if clicks % 2 == 1 then - local line1, col1 = select(3, self.doc:get_selection()) - local line2, col2 = self:resolve_screen_position(x, y) - self.doc:set_selection(line2, col2, line1, col1) - end - else - local line, col = self:resolve_screen_position(x, y) - if keymap.modkeys["ctrl"] then - self.doc:add_selection(mouse_selection(self.doc, clicks, line, col, line, col)) - else - self.doc:set_selection(mouse_selection(self.doc, clicks, line, col, line, col)) - end - self.mouse_selecting = { line, col, clicks = clicks } - end - core.blink_reset() -end - - function DocView:on_mouse_moved(x, y, ...) DocView.super.on_mouse_moved(self, x, y, ...) @@ -280,7 +236,6 @@ function DocView:on_mouse_moved(x, y, ...) if self.mouse_selecting then local l1, c1 = self:resolve_screen_position(x, y) local l2, c2 = table.unpack(self.mouse_selecting) - local clicks = self.mouse_selecting.clicks if keymap.modkeys["ctrl"] then if l1 > l2 then l1, l2 = l2, l1 end self.doc.selections = { } @@ -288,7 +243,7 @@ function DocView:on_mouse_moved(x, y, ...) self.doc:set_selections(i - l1 + 1, i, math.min(c1, #self.doc.lines[i]), i, math.min(c2, #self.doc.lines[i])) end else - self.doc:set_selection(mouse_selection(self.doc, clicks, l1, c1, l2, c2)) + self.doc:set_selection(l1, c1, l2, c2) end end end @@ -338,16 +293,11 @@ end function DocView:draw_line_text(idx, x, y) local default_font = self:get_font() - local subpixel_scale = default_font:subpixel_scale() - local tx, ty = subpixel_scale * x, y + self:get_line_text_y_offset() + local tx, ty = x, y + self:get_line_text_y_offset() for _, type, text in self.doc.highlighter:each_token(idx) do local color = style.syntax[type] local font = style.syntax_fonts[type] or default_font - if config.draw_whitespace then - tx = renderer.draw_text_subpixel(font, text, tx, ty, color, core.replacements, style.syntax.comment) - else - tx = renderer.draw_text_subpixel(font, text, tx, ty, color) - end + tx = renderer.draw_text(font, text, tx, ty, color) end end @@ -357,6 +307,18 @@ function DocView:draw_caret(x, y) end function DocView:draw_line_body(idx, x, y) + -- draw highlight if any selection ends on this line + local draw_highlight = false + for lidx, line1, col1, line2, col2 in self.doc:get_selections(false) do + if line1 == idx then + draw_highlight = true + break + end + end + if draw_highlight and config.highlight_current_line and core.active_view == self then + self:draw_line_highlight(x + self.scroll.x, y) + end + -- draw selection if it overlaps this line for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do if idx >= line1 and idx <= line2 then @@ -366,14 +328,9 @@ function DocView:draw_line_body(idx, x, y) local x1 = x + self:get_col_x_offset(idx, col1) local x2 = x + self:get_col_x_offset(idx, col2) local lh = self:get_line_height() - renderer.draw_rect(x1, y, x2 - x1, lh, style.selection) - end - end - for lidx, line1, col1, line2, col2 in self.doc:get_selections(true) do - -- draw line highlight if caret is on this line - if config.highlight_current_line and (line1 == line2 and col1 == col2) - and line1 == idx and core.active_view == self then - self:draw_line_highlight(x + self.scroll.x, y) + if x1 ~= x2 then + renderer.draw_rect(x1, y, x2 - x1, lh, style.selection) + end end end @@ -403,10 +360,12 @@ function DocView:draw_overlay() local T = config.blink_period for _, line, col in self.doc:get_selections() do if line >= minline and line <= maxline - and (core.blink_timer - core.blink_start) % T < T / 2 and system.window_has_focus() then - local x, y = self:get_line_screen_position(line) - self:draw_caret(x + self:get_col_x_offset(line, col), y) + if config.disable_blink + or (core.blink_timer - core.blink_start) % T < T / 2 then + local x, y = self:get_line_screen_position(line) + self:draw_caret(x + self:get_col_x_offset(line, col), y) + end end end end diff --git a/data/core/init.lua b/data/core/init.lua index caac1737..d07f1cbc 100644 --- a/data/core/init.lua +++ b/data/core/init.lua @@ -17,10 +17,7 @@ local core = {} local function load_session() local ok, t = pcall(dofile, USERDIR .. "/session.lua") - if ok then - return t.recents, t.window, t.window_mode - end - return {} + return ok and t or {} end @@ -30,6 +27,8 @@ local function save_session() fp:write("return {recents=", common.serialize(core.recent_projects), ", window=", common.serialize(table.pack(system.get_window_size())), ", window_mode=", common.serialize(system.get_window_mode()), + ", previous_find=", common.serialize(core.previous_find), + ", previous_replace=", common.serialize(core.previous_replace), "}\n") fp:close() end @@ -80,6 +79,9 @@ function core.open_folder_project(dir_path_abs) if core.set_project_dir(dir_path_abs, core.on_quit_project) then core.root_view:close_all_docviews() update_recents_project("add", dir_path_abs) + if not core.load_project_module() then + command.perform("core:open-log") + end core.on_enter_project(dir_path_abs) end end @@ -320,8 +322,8 @@ local style = require "core.style" ------------------------------- Fonts ---------------------------------------- -- customize fonts: --- style.font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf", 13 * SCALE) --- style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 13 * SCALE) +-- style.font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf", 14 * SCALE) +-- style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 14 * SCALE) -- -- font names used by lite: -- style.font : user interface @@ -342,11 +344,11 @@ local style = require "core.style" -- enable or disable plugin loading setting config entries: --- enable trimwhitespace, otherwise it is disable by default: --- config.trimwhitespace = true +-- enable plugins.trimwhitespace, otherwise it is disable by default: +-- config.plugins.trimwhitespace = true -- -- disable detectindent, otherwise it is enabled by default --- config.detectindent = false +-- config.plugins.detectindent = false ]]) init_file:close() end @@ -394,15 +396,6 @@ function core.remove_project_directory(path) return false end - -local function whitespace_replacements() - local r = renderer.replacements.new() - r:add(" ", "·") - r:add("\t", "»") - return r -end - - local function reload_on_user_module_save() -- auto-realod style when user's module is saved by overriding Doc:Save() local doc_save = Doc.save @@ -435,13 +428,15 @@ function core.init() end do - local recent_projects, window_position, window_mode = load_session() - if window_mode == "normal" then - system.set_window_size(table.unpack(window_position)) - elseif window_mode == "maximized" then + local session = load_session() + if session.window_mode == "normal" then + system.set_window_size(table.unpack(session.window)) + elseif session.window_mode == "maximized" then system.set_window_mode("maximized") end - core.recent_projects = recent_projects + core.recent_projects = session.recents or {} + core.previous_find = session.previous_find or {} + core.previous_replace = session.previous_replace or {} end local project_dir = core.recent_projects[1] or "." @@ -461,7 +456,10 @@ function core.init() project_dir = arg_filename project_dir_explicit = true else - delayed_error = string.format("error: invalid file or directory %q", ARGS[i]) + -- on macOS we can get an argument like "-psn_0_52353" that we just ignore. + if not ARGS[i]:match("^-psn") then + delayed_error = string.format("error: invalid file or directory %q", ARGS[i]) + end end end @@ -494,7 +492,7 @@ function core.init() core.redraw = true core.visited_files = {} core.restart_request = false - core.replacements = whitespace_replacements() + core.quit_request = false core.root_view = RootView() core.command_view = CommandView() @@ -564,10 +562,10 @@ function core.init() end -function core.confirm_close_all(close_fn, ...) +function core.confirm_close_docs(docs, close_fn, ...) local dirty_count = 0 local dirty_name - for _, doc in ipairs(core.docs) do + for _, doc in ipairs(docs or core.docs) do if doc:is_dirty() then dirty_count = dirty_count + 1 dirty_name = doc:get_name() @@ -620,24 +618,6 @@ do end --- DEPRECATED function -core.doc_save_hooks = {} -function core.add_save_hook(fn) - core.error("The function core.add_save_hook is deprecated." .. - " Modules should now directly override the Doc:save function.") - core.doc_save_hooks[#core.doc_save_hooks + 1] = fn -end - - --- DEPRECATED function -function core.on_doc_save(filename) - -- for backward compatibility in modules. Hooks are deprecated, the function Doc:save - -- should be directly overidded. - for _, hook in ipairs(core.doc_save_hooks) do - hook(filename) - end -end - local function quit_with_function(quit_fn, force) if force then delete_temp_files() @@ -645,12 +625,12 @@ local function quit_with_function(quit_fn, force) save_session() quit_fn() else - core.confirm_close_all(quit_with_function, quit_fn, true) + core.confirm_close_docs(core.docs, quit_with_function, quit_fn, true) end end function core.quit(force) - quit_with_function(os.exit, force) + quit_with_function(function() core.quit_request = true end, force) end @@ -679,8 +659,8 @@ local function check_plugin_version(filename) -- Future versions will look only at the mod-version tag. local version = line:match('%-%-%s*lite%-xl%s*(%d+%.%d+)$') if version then - -- we consider the version tag 1.16 equivalent to mod-version:1 - version_match = (version == '1.16' and MOD_VERSION == "1") + -- we consider the version tag 2.0 equivalent to mod-version:2 + version_match = (version == '2.0' and MOD_VERSION == "2") break end end @@ -695,24 +675,30 @@ function core.load_plugins() userdir = {dir = USERDIR, plugins = {}}, datadir = {dir = DATADIR, plugins = {}}, } - for _, root_dir in ipairs {USERDIR, DATADIR} do + local files, ordered = {}, {} + for _, root_dir in ipairs {DATADIR, USERDIR} do local plugin_dir = root_dir .. "/plugins" - local files = system.list_dir(plugin_dir) - for _, filename in ipairs(files or {}) do - local basename = filename:match("(.-)%.lua$") or filename - local is_lua_file, version_match = check_plugin_version(plugin_dir .. '/' .. filename) - if is_lua_file then - if not version_match then - core.log_quiet("Version mismatch for plugin %q from %s", basename, plugin_dir) - local ls = refused_list[root_dir == USERDIR and 'userdir' or 'datadir'].plugins - ls[#ls + 1] = filename - elseif config.plugins[basename] ~= false then - local modname = "plugins." .. basename - local ok = core.try(require, modname) - if ok then core.log_quiet("Loaded plugin %q from %s", basename, plugin_dir) end - if not ok then - no_errors = false - end + for _, filename in ipairs(system.list_dir(plugin_dir) or {}) do + if not files[filename] then table.insert(ordered, filename) end + files[filename] = plugin_dir -- user plugins will always replace system plugins + end + end + table.sort(ordered) + + for _, filename in ipairs(ordered) do + local plugin_dir, basename = files[filename], filename:match("(.-)%.lua$") or filename + local is_lua_file, version_match = check_plugin_version(plugin_dir .. '/' .. filename) + if is_lua_file then + if not version_match then + core.log_quiet("Version mismatch for plugin %q from %s", basename, plugin_dir) + local list = refused_list[plugin_dir:find(USERDIR, 1, true) == 1 and 'userdir' or 'datadir'].plugins + table.insert(list, filename) + end + if version_match and config.plugins[basename] ~= false then + local ok = core.try(require, "plugins." .. basename) + if ok then core.log_quiet("Loaded plugin %q from %s", basename, plugin_dir) end + if not ok then + no_errors = false end end end @@ -759,8 +745,12 @@ end function core.set_active_view(view) assert(view, "Tried to set active view to nil") - if core.active_view and core.active_view.force_focus then return end if view ~= core.active_view then + if core.active_view and core.active_view.force_focus then + core.next_active_view = view + return + end + core.next_active_view = nil if view.doc and view.doc.filename then core.set_visited(view.doc.filename) end @@ -810,10 +800,30 @@ function core.normalize_to_project_dir(filename) end +-- The function below works like system.absolute_path except it +-- doesn't fail if the file does not exist. We consider that the +-- current dir is core.project_dir so relative filename are considered +-- to be in core.project_dir. +-- Please note that .. or . in the filename are not taken into account. +-- This function should get only filenames normalized using +-- common.normalize_path function. +function core.project_absolute_path(filename) + if filename:match('^%a:\\') or filename:find('/', 1, true) == 1 then + return filename + else + return core.project_dir .. PATHSEP .. filename + end +end + + function core.open_doc(filename) + local new_file = not filename or not system.get_file_info(filename) + local abs_filename if filename then + -- normalize filename and set absolute filename then -- try to find existing doc for filename - local abs_filename = system.absolute_path(filename) + filename = core.normalize_to_project_dir(filename) + abs_filename = core.project_absolute_path(filename) for _, doc in ipairs(core.docs) do if doc.abs_filename and abs_filename == doc.abs_filename then return doc @@ -821,8 +831,7 @@ function core.open_doc(filename) end end -- no existing doc for filename; create new - filename = filename and core.normalize_to_project_dir(filename) - local doc = Doc(filename) + local doc = Doc(filename, abs_filename, new_file) table.insert(core.docs, doc) core.log_quiet(filename and "Opened doc \"%s\"" or "Opened new doc", filename) return doc @@ -871,6 +880,23 @@ function core.error(...) end +function core.get_log(i) + if i == nil then + local r = {} + for _, item in ipairs(core.log_items) do + table.insert(r, core.get_log(item)) + end + return table.concat(r, "\n") + end + local item = type(i) == "number" and core.log_items[i] or i + local text = string.format("[%s] %s at %s", os.date(nil, item.time), item.text, item.at) + if item.info then + text = string.format("%s\n%s\n", text, item.info) + end + return text +end + + function core.try(fn, ...) local err local ok, res = xpcall(fn, function(msg) @@ -896,11 +922,15 @@ function core.on_event(type, ...) elseif type == "mousemoved" then core.root_view:on_mouse_moved(...) elseif type == "mousepressed" then - core.root_view:on_mouse_pressed(...) + if not core.root_view:on_mouse_pressed(...) then + did_keymap = keymap.on_mouse_pressed(...) + end elseif type == "mousereleased" then core.root_view:on_mouse_released(...) elseif type == "mousewheel" then - core.root_view:on_mouse_wheel(...) + if not core.root_view:on_mouse_wheel(...) then + did_keymap = keymap.on_mouse_wheel(...) + end elseif type == "resized" then core.window_mode = system.get_window_mode() elseif type == "minimized" or type == "maximized" or type == "restored" then @@ -1027,7 +1057,7 @@ function core.run() core.frame_start = system.get_time() local did_redraw = core.step() local need_more_work = run_threads() - if core.restart_request then break end + if core.restart_request or core.quit_request then break end if not did_redraw and not need_more_work then idle_iterations = idle_iterations + 1 -- do not wait of events at idle_iterations = 1 to give a chance at core.step to run diff --git a/data/core/keymap-macos.lua b/data/core/keymap-macos.lua index e233bb2e..b0bd41a5 100644 --- a/data/core/keymap-macos.lua +++ b/data/core/keymap-macos.lua @@ -32,6 +32,8 @@ local function keymap_macos(keymap) ["cmd+7"] = "root:switch-to-tab-7", ["cmd+8"] = "root:switch-to-tab-8", ["cmd+9"] = "root:switch-to-tab-9", + ["wheel"] = "root:scroll", + ["cmd+f"] = "find-replace:find", ["cmd+r"] = "find-replace:replace", ["f3"] = "find-replace:repeat-find", @@ -52,23 +54,27 @@ local function keymap_macos(keymap) ["shift+tab"] = "doc:unindent", ["backspace"] = "doc:backspace", ["shift+backspace"] = "doc:backspace", - ["cmd+backspace"] = "doc:delete-to-previous-word-start", + ["option+backspace"] = "doc:delete-to-previous-word-start", ["cmd+shift+backspace"] = "doc:delete-to-previous-word-start", + ["cmd+backspace"] = "doc:delete-to-start-of-indentation", ["delete"] = "doc:delete", ["shift+delete"] = "doc:delete", - ["cmd+delete"] = "doc:delete-to-next-word-end", + ["option+delete"] = "doc:delete-to-next-word-end", ["cmd+shift+delete"] = "doc:delete-to-next-word-end", + ["cmd+delete"] = "doc:delete-to-end-of-line", ["return"] = { "command:submit", "doc:newline", "dialog:select" }, ["keypad enter"] = { "command:submit", "doc:newline", "dialog:select" }, ["cmd+return"] = "doc:newline-below", ["cmd+shift+return"] = "doc:newline-above", ["cmd+j"] = "doc:join-lines", ["cmd+a"] = "doc:select-all", - ["cmd+d"] = { "find-replace:select-next", "doc:select-word" }, + ["cmd+d"] = { "find-replace:select-add-next", "doc:select-word" }, + ["cmd+f3"] = "find-replace:select-next", ["cmd+l"] = "doc:select-lines", + ["cmd+shift+l"] = { "find-replace:select-add-all", "doc:select-word" }, ["cmd+/"] = "doc:toggle-line-comments", - ["cmd+up"] = "doc:move-lines-up", - ["cmd+down"] = "doc:move-lines-down", + ["option+up"] = "doc:move-lines-up", + ["option+down"] = "doc:move-lines-down", ["cmd+shift+d"] = "doc:duplicate-lines", ["cmd+shift+k"] = "doc:delete-lines", @@ -76,33 +82,42 @@ local function keymap_macos(keymap) ["right"] = { "doc:move-to-next-char", "dialog:next-entry"}, ["up"] = { "command:select-previous", "doc:move-to-previous-line" }, ["down"] = { "command:select-next", "doc:move-to-next-line" }, - ["cmd+left"] = "doc:move-to-previous-word-start", - ["cmd+right"] = "doc:move-to-next-word-end", + ["option+left"] = "doc:move-to-previous-word-start", + ["option+right"] = "doc:move-to-next-word-end", + ["cmd+left"] = "doc:move-to-start-of-indentation", + ["cmd+right"] = "doc:move-to-end-of-line", ["cmd+["] = "doc:move-to-previous-block-start", ["cmd+]"] = "doc:move-to-next-block-end", - ["home"] = "doc:move-to-start-of-line", + ["home"] = "doc:move-to-start-of-indentation", ["end"] = "doc:move-to-end-of-line", - ["cmd+home"] = "doc:move-to-start-of-doc", - ["cmd+end"] = "doc:move-to-end-of-doc", + ["cmd+up"] = "doc:move-to-start-of-doc", + ["cmd+down"] = "doc:move-to-end-of-doc", ["pageup"] = "doc:move-to-previous-page", ["pagedown"] = "doc:move-to-next-page", + ["shift+1lclick"] = "doc:select-to-cursor", + ["ctrl+1lclick"] = "doc:split-cursor", + ["1lclick"] = "doc:set-cursor", + ["2lclick"] = "doc:set-cursor-word", + ["3lclick"] = "doc:set-cursor-line", ["shift+left"] = "doc:select-to-previous-char", ["shift+right"] = "doc:select-to-next-char", ["shift+up"] = "doc:select-to-previous-line", ["shift+down"] = "doc:select-to-next-line", - ["cmd+shift+left"] = "doc:select-to-previous-word-start", - ["cmd+shift+right"] = "doc:select-to-next-word-end", + ["option+shift+left"] = "doc:select-to-previous-word-start", + ["option+shift+right"] = "doc:select-to-next-word-end", + ["cmd+shift+left"] = "doc:select-to-start-of-indentation", + ["cmd+shift+right"] = "doc:select-to-end-of-line", ["cmd+shift+["] = "doc:select-to-previous-block-start", ["cmd+shift+]"] = "doc:select-to-next-block-end", - ["shift+home"] = "doc:select-to-start-of-line", + ["shift+home"] = "doc:select-to-start-of-indentation", ["shift+end"] = "doc:select-to-end-of-line", - ["cmd+shift+home"] = "doc:select-to-start-of-doc", - ["cmd+shift+end"] = "doc:select-to-end-of-doc", + ["cmd+shift+up"] = "doc:select-to-start-of-doc", + ["cmd+shift+down"] = "doc:select-to-end-of-doc", ["shift+pageup"] = "doc:select-to-previous-page", ["shift+pagedown"] = "doc:select-to-next-page", - ["cmd+shift+up"] = "doc:create-cursor-previous-line", - ["cmd+shift+down"] = "doc:create-cursor-next-line" + ["cmd+option+up"] = "doc:create-cursor-previous-line", + ["cmd+option+down"] = "doc:create-cursor-next-line" } end diff --git a/data/core/keymap.lua b/data/core/keymap.lua index c402f37b..fd552f19 100644 --- a/data/core/keymap.lua +++ b/data/core/keymap.lua @@ -1,11 +1,12 @@ local command = require "core.command" +local config = require "core.config" local keymap = {} keymap.modkeys = {} keymap.map = {} keymap.reverse_map = {} -local macos = rawget(_G, "MACOS_RESOURCES") +local macos = PLATFORM == "Mac OS X" -- Thanks to mathewmariani, taken from his lite-macos github repository. local modkeys_os = require("core.modkeys-" .. (macos and "macos" or "generic")) @@ -30,7 +31,8 @@ function keymap.add_direct(map) end keymap.map[stroke] = commands for _, cmd in ipairs(commands) do - keymap.reverse_map[cmd] = stroke + keymap.reverse_map[cmd] = keymap.reverse_map[cmd] or {} + table.insert(keymap.reverse_map[cmd], stroke) end end end @@ -52,18 +54,43 @@ function keymap.add(map, overwrite) end end for _, cmd in ipairs(commands) do - keymap.reverse_map[cmd] = stroke + keymap.reverse_map[cmd] = keymap.reverse_map[cmd] or {} + table.insert(keymap.reverse_map[cmd], stroke) end end end -function keymap.get_binding(cmd) - return keymap.reverse_map[cmd] +local function remove_only(tbl, k, v) + for key, values in pairs(tbl) do + if key == k then + if v then + for i, value in ipairs(values) do + if value == v then + table.remove(values, i) + end + end + else + tbl[key] = nil + end + break + end + end end -function keymap.on_key_pressed(k) +function keymap.unbind(key, cmd) + remove_only(keymap.map, key, cmd) + remove_only(keymap.reverse_map, cmd, key) +end + + +function keymap.get_binding(cmd) + return table.unpack(keymap.reverse_map[cmd] or {}) +end + + +function keymap.on_key_pressed(k, ...) local mk = modkey_map[k] if mk then keymap.modkeys[mk] = true @@ -73,18 +100,30 @@ function keymap.on_key_pressed(k) end else local stroke = key_to_stroke(k) - local commands = keymap.map[stroke] + local commands, performed = keymap.map[stroke] if commands then for _, cmd in ipairs(commands) do - local performed = command.perform(cmd) + performed = command.perform(cmd, ...) if performed then break end end - return true + return performed end end return false end +function keymap.on_mouse_wheel(delta, ...) + return not (keymap.on_key_pressed("wheel" .. (delta > 0 and "up" or "down"), delta, ...) + or keymap.on_key_pressed("wheel", delta, ...)) +end + +function keymap.on_mouse_pressed(button, x, y, clicks) + local click_number = (((clicks - 1) % config.max_clicks) + 1) + return not (keymap.on_key_pressed(click_number .. button:sub(1,1) .. "click", x, y, clicks) or + keymap.on_key_pressed(button:sub(1,1) .. "click", x, y, clicks) or + keymap.on_key_pressed(click_number .. "click", x, y, clicks) or + keymap.on_key_pressed("click", x, y, clicks)) +end function keymap.on_key_released(k) local mk = modkey_map[k] @@ -108,6 +147,7 @@ keymap.add_direct { ["ctrl+shift+c"] = "core:change-project-folder", ["ctrl+shift+o"] = "core:open-project-folder", ["alt+return"] = "core:toggle-fullscreen", + ["f11"] = "core:toggle-fullscreen", ["alt+shift+j"] = "root:split-left", ["alt+shift+l"] = "root:split-right", @@ -132,6 +172,7 @@ keymap.add_direct { ["alt+7"] = "root:switch-to-tab-7", ["alt+8"] = "root:switch-to-tab-8", ["alt+9"] = "root:switch-to-tab-9", + ["wheel"] = "root:scroll", ["ctrl+f"] = "find-replace:find", ["ctrl+r"] = "find-replace:replace", @@ -167,8 +208,10 @@ keymap.add_direct { ["ctrl+shift+return"] = "doc:newline-above", ["ctrl+j"] = "doc:join-lines", ["ctrl+a"] = "doc:select-all", - ["ctrl+d"] = { "find-replace:select-next", "doc:select-word" }, + ["ctrl+d"] = { "find-replace:select-add-next", "doc:select-word" }, + ["ctrl+f3"] = "find-replace:select-next", ["ctrl+l"] = "doc:select-lines", + ["ctrl+shift+l"] = { "find-replace:select-add-all", "doc:select-word" }, ["ctrl+/"] = "doc:toggle-line-comments", ["ctrl+up"] = "doc:move-lines-up", ["ctrl+down"] = "doc:move-lines-down", @@ -183,13 +226,18 @@ keymap.add_direct { ["ctrl+right"] = "doc:move-to-next-word-end", ["ctrl+["] = "doc:move-to-previous-block-start", ["ctrl+]"] = "doc:move-to-next-block-end", - ["home"] = "doc:move-to-start-of-line", + ["home"] = "doc:move-to-start-of-indentation", ["end"] = "doc:move-to-end-of-line", ["ctrl+home"] = "doc:move-to-start-of-doc", ["ctrl+end"] = "doc:move-to-end-of-doc", ["pageup"] = "doc:move-to-previous-page", ["pagedown"] = "doc:move-to-next-page", + ["shift+1lclick"] = "doc:select-to-cursor", + ["ctrl+1lclick"] = "doc:split-cursor", + ["1lclick"] = "doc:set-cursor", + ["2lclick"] = "doc:set-cursor-word", + ["3lclick"] = "doc:set-cursor-line", ["shift+left"] = "doc:select-to-previous-char", ["shift+right"] = "doc:select-to-next-char", ["shift+up"] = "doc:select-to-previous-line", @@ -198,7 +246,7 @@ keymap.add_direct { ["ctrl+shift+right"] = "doc:select-to-next-word-end", ["ctrl+shift+["] = "doc:select-to-previous-block-start", ["ctrl+shift+]"] = "doc:select-to-next-block-end", - ["shift+home"] = "doc:select-to-start-of-line", + ["shift+home"] = "doc:select-to-start-of-indentation", ["shift+end"] = "doc:select-to-end-of-line", ["ctrl+shift+home"] = "doc:select-to-start-of-doc", ["ctrl+shift+end"] = "doc:select-to-end-of-doc", diff --git a/data/core/logview.lua b/data/core/logview.lua index d7142fb5..1ea0e43e 100644 --- a/data/core/logview.lua +++ b/data/core/logview.lua @@ -1,14 +1,45 @@ local core = require "core" +local common = require "core.common" local style = require "core.style" local View = require "core.view" +local function lines(text) + if text == "" then return 0 end + local l = 1 + for _ in string.gmatch(text, "\n") do + l = l + 1 + end + return l +end + + +local item_height_result = {} + + +local function get_item_height(item) + local h = item_height_result[item] + if not h then + h = {} + local l = 1 + lines(item.text) + lines(item.info or "") + h.normal = style.font:get_height() + style.padding.y + h.expanded = l * style.font:get_height() + style.padding.y + h.current = h.normal + h.target = h.current + item_height_result[item] = h + end + return h +end + + local LogView = View:extend() +LogView.context = "session" function LogView:new() LogView.super.new(self) self.last_item = core.log_items[#core.log_items] + self.expanding = {} self.scrollable = true self.yoffset = 0 end @@ -19,6 +50,55 @@ function LogView:get_name() end +local function is_expanded(item) + local item_height = get_item_height(item) + return item_height.target == item_height.expanded +end + + +function LogView:expand_item(item) + item = get_item_height(item) + item.target = item.target == item.expanded and item.normal or item.expanded + table.insert(self.expanding, item) +end + + +function LogView:each_item() + local x, y = self:get_content_offset() + y = y + style.padding.y + self.yoffset + return coroutine.wrap(function() + for i = #core.log_items, 1, -1 do + local item = core.log_items[i] + local h = get_item_height(item).current + coroutine.yield(i, item, x, y, self.size.x, h) + y = y + h + end + end) +end + + +function LogView:on_mouse_moved(px, py, ...) + LogView.super.on_mouse_moved(self, px, py, ...) + local hovered = false + for _, item, x, y, w, h in self:each_item() do + if px >= x and py >= y and px < x + w and py < y + h then + hovered = true + self.hovered_item = item + break + end + end + if not hovered then self.hovered_item = nil end +end + + +function LogView:on_mouse_pressed(button, mx, my, clicks) + if LogView.super.on_mouse_pressed(self, button, mx, my, clicks) then return end + if self.hovered_item then + self:expand_item(self.hovered_item) + end +end + + function LogView:update() local item = core.log_items[#core.log_items] if self.last_item ~= item then @@ -27,6 +107,14 @@ function LogView:update() self.yoffset = -(style.font:get_height() + style.padding.y) end + local expanding = self.expanding[1] + if expanding then + self:move_towards(expanding, "current", expanding.target) + if expanding.current == expanding.target then + table.remove(self.expanding, 1) + end + end + self:move_towards("yoffset", 0) LogView.super.update(self) @@ -35,38 +123,48 @@ end local function draw_text_multiline(font, text, x, y, color) local th = font:get_height() - local resx, resy = x, y + local resx = x for line in text:gmatch("[^\n]+") do - resy = y resx = renderer.draw_text(style.font, line, x, y, color) y = y + th end - return resx, resy + return resx, y end function LogView:draw() self:draw_background(style.background) - local ox, oy = self:get_content_offset() local th = style.font:get_height() - local y = oy + style.padding.y + self.yoffset - - for i = #core.log_items, 1, -1 do - local x = ox + style.padding.x - local item = core.log_items[i] - local time = os.date(nil, item.time) - x = renderer.draw_text(style.font, time, x, y, style.dim) + local lh = th + style.padding.y -- for one line + for _, item, x, y, w in self:each_item() do x = x + style.padding.x - local subx = x - x, y = draw_text_multiline(style.font, item.text, x, y, style.text) - renderer.draw_text(style.font, " at " .. item.at, x, y, style.dim) - y = y + th - if item.info then - subx, y = draw_text_multiline(style.font, item.info, subx, y, style.dim) - y = y + th + + local time = os.date(nil, item.time) + x = common.draw_text(style.font, style.dim, time, "left", x, y, w, lh) + x = x + style.padding.x + + x = common.draw_text(style.code_font, style.dim, is_expanded(item) and "-" or "+", "left", x, y, w, lh) + x = x + style.padding.x + w = w - (x - self:get_content_offset()) + + if is_expanded(item) then + y = y + common.round(style.padding.y / 2) + _, y = draw_text_multiline(style.font, item.text, x, y, style.text) + + local at = "at " .. common.home_encode(item.at) + _, y = common.draw_text(style.font, style.dim, at, "left", x, y, w, lh) + + if item.info then + _, y = draw_text_multiline(style.font, item.info, x, y, style.dim) + end + else + local line, has_newline = string.match(item.text, "([^\n]+)(\n?)") + if has_newline ~= "" then + line = line .. " ..." + end + _, y = common.draw_text(style.font, style.text, line, "left", x, y, w, lh) end - y = y + style.padding.y end end diff --git a/data/core/nagview.lua b/data/core/nagview.lua index 6d6f89f4..3d448cd4 100644 --- a/data/core/nagview.lua +++ b/data/core/nagview.lua @@ -193,7 +193,8 @@ function NagView:next() self:change_hovered(common.find_index(self.options, "default_yes")) end self.force_focus = self.message ~= nil - core.set_active_view(self.message ~= nil and self or core.last_active_view) + core.set_active_view(self.message ~= nil and self or + core.next_active_view or core.last_active_view) end function NagView:show(title, message, options, on_select) diff --git a/data/core/object.lua b/data/core/object.lua index af41b7e9..0941ce5d 100644 --- a/data/core/object.lua +++ b/data/core/object.lua @@ -20,17 +20,6 @@ function Object:extend() end -function Object:implement(...) - for _, cls in pairs({...}) do - for k, v in pairs(cls) do - if self[k] == nil and type(v) == "function" then - self[k] = v - end - end - end -end - - function Object:is(T) local mt = getmetatable(self) while mt do diff --git a/data/core/regex.lua b/data/core/regex.lua index 19c59164..69203cbd 100644 --- a/data/core/regex.lua +++ b/data/core/regex.lua @@ -1,5 +1,5 @@ --- So that in addition to regex.gsub(pattern, string), we can also do +-- So that in addition to regex.gsub(pattern, string), we can also do -- pattern:gsub(string). regex.__index = function(table, key) return regex[key]; end @@ -9,7 +9,7 @@ regex.match = function(pattern_string, string, offset, options) return regex.cmatch(pattern, string, offset or 1, options or 0) end --- Will iterate back through any UTF-8 bytes so that we don't replace bits +-- Will iterate back through any UTF-8 bytes so that we don't replace bits -- mid character. local function previous_character(str, index) local byte @@ -23,7 +23,7 @@ end -- Moves to the end of the identified character. local function end_character(str, index) local byte = string.byte(str, index + 1) - while byte >= 128 and byte < 192 do + while byte and byte >= 128 and byte < 192 do index = index + 1 byte = string.byte(str, index + 1) end @@ -32,7 +32,7 @@ end -- Build off matching. For now, only support basic replacements, but capture -- groupings should be doable. We can even have custom group replacements and --- transformations and stuff in lua. Currently, this takes group replacements +-- transformations and stuff in lua. Currently, this takes group replacements -- as \1 - \9. -- Should work on UTF-8 text. regex.gsub = function(pattern_string, str, replacement) @@ -48,8 +48,8 @@ regex.gsub = function(pattern_string, str, replacement) if #indices > 2 then for i = 1, (#indices/2 - 1) do currentReplacement = string.gsub( - currentReplacement, - "\\" .. i, + currentReplacement, + "\\" .. i, str:sub(indices[i*2+1], end_character(str,indices[i*2+2]-1)) ) end @@ -57,10 +57,10 @@ regex.gsub = function(pattern_string, str, replacement) currentReplacement = string.gsub(currentReplacement, "\\%d", "") table.insert(replacements, { indices[1], #currentReplacement+indices[1] }) if indices[1] > 1 then - result = result .. + result = result .. str:sub(1, previous_character(str, indices[1])) .. currentReplacement else - result = result .. currentReplacement + result = result .. currentReplacement end str = str:sub(indices[2]) end diff --git a/data/core/rootview.lua b/data/core/rootview.lua index 36ab148d..07f8b7bf 100644 --- a/data/core/rootview.lua +++ b/data/core/rootview.lua @@ -5,7 +5,6 @@ local style = require "core.style" local keymap = require "core.keymap" local Object = require "core.object" local View = require "core.view" -local CommandView = require "core.commandview" local NagView = require "core.nagview" local DocView = require "core.docview" @@ -143,7 +142,14 @@ function Node:remove_view(root, view) local parent = self:get_parent_node(root) local is_a = (parent.a == self) local other = parent[is_a and "b" or "a"] - if other:get_locked_size() then + local locked_size_x, locked_size_y = other:get_locked_size() + local locked_size + if parent.type == "hsplit" then + locked_size = locked_size_x + else + locked_size = locked_size_y + end + if self.is_primary_node or locked_size then self.views = {} self:add_view(EmptyView()) else @@ -240,12 +246,15 @@ end function Node:get_divider_overlapping_point(px, py) if self.type ~= "leaf" then - local p = 6 - local x, y, w, h = self:get_divider_rect() - x, y = x - p, y - p - w, h = w + p * 2, h + p * 2 - if px > x and py > y and px < x + w and py < y + h then - return self + local axis = self.type == "hsplit" and "x" or "y" + if self.a:is_resizable(axis) and self.b:is_resizable(axis) then + local p = 6 + local x, y, w, h = self:get_divider_rect() + x, y = x - p, y - p + w, h = w + p * 2, h + p * 2 + if px > x and py > y and px < x + w and py < y + h then + return self + end end return self.a:get_divider_overlapping_point(px, py) or self.b:get_divider_overlapping_point(px, py) @@ -259,7 +268,7 @@ end function Node:get_tab_overlapping_point(px, py) - if #self.views == 1 then return nil end + if not self:should_show_tabs() then return nil end local tabs_number = self:get_visible_tabs_number() local x1, y1, w, h = self:get_tab_rect(self.tab_offset) local x2, y2 = self:get_tab_rect(self.tab_offset + tabs_number) @@ -269,6 +278,19 @@ function Node:get_tab_overlapping_point(px, py) end +function Node:should_show_tabs() + if self.locked then return false end + local dn = core.root_view.dragged_node + if #self.views > 1 + or (dn and dn.dragging) then -- show tabs while dragging + return true + elseif config.always_show_tabs then + return not self.views[1]:is(EmptyView) + end + return false +end + + local function close_button_location(x, w) local cw = style.icon_font:get_width("C") local pad = style.padding.y @@ -412,7 +434,7 @@ end function Node:update_layout() if self.type == "leaf" then local av = self.active_view - if #self.views > 1 then + if self:should_show_tabs() then local _, _, _, th = self:get_tab_rect(1) av.position.x, av.position.y = self.position.x, self.position.y + th av.size.x, av.size.y = self.size.x, self.size.y - th @@ -494,6 +516,53 @@ function Node:update() end end +function Node:draw_tab(text, is_active, is_hovered, is_close_hovered, x, y, w, h, standalone) + local ds = style.divider_size + local dots_width = style.font:get_width("…") + local color = style.dim + local padding_y = style.padding.y + renderer.draw_rect(x + w, y + padding_y, ds, h - padding_y * 2, style.dim) + if standalone then + renderer.draw_rect(x-1, y-1, w+2, h+2, style.background2) + end + if is_active then + color = style.text + renderer.draw_rect(x, y, w, h, style.background) + renderer.draw_rect(x + w, y, ds, h, style.divider) + renderer.draw_rect(x - ds, y, ds, h, style.divider) + end + local cx, cw, cspace = close_button_location(x, w) + local show_close_button = ((is_active or is_hovered) and not standalone and config.tab_close_button) + if show_close_button then + local close_style = is_close_hovered and style.text or style.dim + common.draw_text(style.icon_font, close_style, "C", nil, cx, y, 0, h) + end + if is_hovered then + color = style.text + end + local padx = style.padding.x + -- Normally we should substract "cspace" from text_avail_width and from the + -- clipping width. It is the padding space we give to the left and right of the + -- close button. However, since we are using dots to terminate filenames, we + -- choose to ignore "cspace" accepting that the text can possibly "touch" the + -- close button. + local text_avail_width = cx - x - padx + core.push_clip_rect(x, y, cx - x, h) + x, w = x + padx, w - padx * 2 + local align = "center" + if style.font:get_width(text) > text_avail_width then + align = "left" + for i = 1, #text do + local reduced_text = text:sub(1, #text - i) + if style.font:get_width(reduced_text) + dots_width <= text_avail_width then + text = reduced_text .. "…" + break + end + end + end + common.draw_text(style.font, color, text, align, x, y, w, h) + core.pop_clip_rect() +end function Node:draw_tabs() local x, y, w, h, scroll_padding = self:get_scroll_button_rect(1) @@ -518,47 +587,9 @@ function Node:draw_tabs() for i = self.tab_offset, self.tab_offset + tabs_number - 1 do local view = self.views[i] local x, y, w, h = self:get_tab_rect(i) - local text = view:get_name() - local color = style.dim - local padding_y = style.padding.y - renderer.draw_rect(x + w, y + padding_y, ds, h - padding_y * 2, style.dim) - if view == self.active_view then - color = style.text - renderer.draw_rect(x, y, w, h, style.background) - renderer.draw_rect(x + w, y, ds, h, style.divider) - renderer.draw_rect(x - ds, y, ds, h, style.divider) - end - local cx, cw, cspace = close_button_location(x, w) - local show_close_button = ((view == self.active_view or i == self.hovered_tab) and config.tab_close_button) - if show_close_button then - local close_style = self.hovered_close == i and style.text or style.dim - common.draw_text(style.icon_font, close_style, "C", nil, cx, y, 0, h) - end - if i == self.hovered_tab then - color = style.text - end - local padx = style.padding.x - -- Normally we should substract "cspace" from text_avail_width and from the - -- clipping width. It is the padding space we give to the left and right of the - -- close button. However, since we are using dots to terminate filenames, we - -- choose to ignore "cspace" accepting that the text can possibly "touch" the - -- close button. - local text_avail_width = cx - x - padx - core.push_clip_rect(x, y, cx - x, h) - x, w = x + padx, w - padx * 2 - local align = "center" - if style.font:get_width(text) > text_avail_width then - align = "left" - for i = 1, #text do - local reduced_text = text:sub(1, #text - i) - if style.font:get_width(reduced_text) + dots_width <= text_avail_width then - text = reduced_text .. "…" - break - end - end - end - common.draw_text(style.font, color, text, align, x, y, w, h) - core.pop_clip_rect() + self:draw_tab(view:get_name(), view == self.active_view, + i == self.hovered_tab, i == self.hovered_close, + x, y, w, h) end core.pop_clip_rect() @@ -567,7 +598,7 @@ end function Node:draw() if self.type == "leaf" then - if #self.views > 1 then + if self:should_show_tabs() then self:draw_tabs() end local pos, size = self.active_view.position, self.active_view.size @@ -591,23 +622,37 @@ function Node:is_empty() end -function Node:close_all_docviews() +function Node:close_all_docviews(keep_active) + local node_active_view = self.active_view + local lost_active_view = false if self.type == "leaf" then local i = 1 while i <= #self.views do local view = self.views[i] - if view:is(DocView) and not view:is(CommandView) then + if view.context == "session" and (not keep_active or view ~= self.active_view) then table.remove(self.views, i) + if view == node_active_view then + lost_active_view = true + end else i = i + 1 end end + self.tab_offset = 1 if #self.views == 0 and self.is_primary_node then + -- if we are not the primary view and we had the active view it doesn't + -- matter to reattribute the active view because, within the close_all_docviews + -- top call, the primary node will take the active view anyway. + -- Set the empty view and takes the active view. self:add_view(EmptyView()) + elseif #self.views > 0 and lost_active_view then + -- In practice we never get there but if a view remain we need + -- to reset the Node's active view. + self:set_active_view(self.views[1]) end else - self.a:close_all_docviews() - self.b:close_all_docviews() + self.a:close_all_docviews(keep_active) + self.b:close_all_docviews(keep_active) if self.a:is_empty() and not self.a.is_primary_node then self:consume(self.b) elseif self.b:is_empty() and not self.b.is_primary_node then @@ -670,6 +715,62 @@ function Node:resize(axis, value) end +function Node:get_split_type(mouse_x, mouse_y) + local x, y = self.position.x, self.position.y + local w, h = self.size.x, self.size.y + local _, _, _, tab_h = self:get_scroll_button_rect(1) + y = y + tab_h + h = h - tab_h + + local local_mouse_x = mouse_x - x + local local_mouse_y = mouse_y - y + + if local_mouse_y < 0 then + return "tab" + else + local left_pct = local_mouse_x * 100 / w + local top_pct = local_mouse_y * 100 / h + if left_pct <= 30 then + return "left" + elseif left_pct >= 70 then + return "right" + elseif top_pct <= 30 then + return "up" + elseif top_pct >= 70 then + return "down" + end + return "middle" + end +end + + +function Node:get_drag_overlay_tab_position(x, y, dragged_node, dragged_index) + local tab_index = self:get_tab_overlapping_point(x, y) + if not tab_index then + local first_tab_x = self:get_tab_rect(1) + if x < first_tab_x then + -- mouse before first visible tab + tab_index = self.tab_offset or 1 + else + -- mouse after last visible tab + tab_index = self:get_visible_tabs_number() + (self.tab_offset - 1 or 0) + end + end + local tab_x, tab_y, tab_w, tab_h = self:get_tab_rect(tab_index) + if x > tab_x + tab_w / 2 and tab_index <= #self.views then + -- use next tab + tab_x = tab_x + tab_w + tab_index = tab_index + 1 + end + if self == dragged_node and dragged_index and tab_index > dragged_index then + -- the tab we are moving is counted in tab_index + tab_index = tab_index - 1 + tab_x = tab_x - tab_w + end + return tab_index, tab_x, tab_y, tab_w, tab_h +end + + local RootView = View:extend() function RootView:new() @@ -677,6 +778,14 @@ function RootView:new() self.root_node = Node() self.deferred_draws = {} self.mouse = { x = 0, y = 0 } + self.drag_overlay = { x = 0, y = 0, w = 0, h = 0, visible = false, opacity = 0, + base_color = style.drag_overlay, + color = { table.unpack(style.drag_overlay) } } + self.drag_overlay.to = { x = 0, y = 0, w = 0, h = 0 } + self.drag_overlay_tab = { x = 0, y = 0, w = 0, h = 0, visible = false, opacity = 0, + base_color = style.drag_overlay_tab, + color = { table.unpack(style.drag_overlay_tab) } } + self.drag_overlay_tab.to = { x = 0, y = 0, w = 0, h = 0 } end @@ -733,8 +842,8 @@ function RootView:open_doc(doc) end -function RootView:close_all_docviews() - self.root_node:close_all_docviews() +function RootView:close_all_docviews(keep_active) + self.root_node:close_all_docviews(keep_active) end @@ -748,38 +857,101 @@ function RootView:on_mouse_pressed(button, x, y, clicks) local div = self.root_node:get_divider_overlapping_point(x, y) if div then self.dragged_divider = div - return + return true end local node = self.root_node:get_child_overlapping_point(x, y) if node.hovered_scroll_button > 0 then node:scroll_tabs(node.hovered_scroll_button) - return + return true end local idx = node:get_tab_overlapping_point(x, y) if idx then if button == "middle" or node.hovered_close == idx then node:close_view(self.root_node, node.views[idx]) + return true else - self.dragged_node = { node, idx } + if button == "left" then + self.dragged_node = { node = node, idx = idx, dragging = false, drag_start_x = x, drag_start_y = y} + end node:set_active_view(node.views[idx]) + return true end - else + elseif not self.dragged_node then -- avoid sending on_mouse_pressed events when dragging tabs core.set_active_view(node.active_view) if not self.on_view_mouse_pressed(button, x, y, clicks) then - node.active_view:on_mouse_pressed(button, x, y, clicks) + return node.active_view:on_mouse_pressed(button, x, y, clicks) end end end -function RootView:on_mouse_released(...) +function RootView:get_overlay_base_color(overlay) + if overlay == self.drag_overlay then + return style.drag_overlay + else + return style.drag_overlay_tab + end +end + + +function RootView:set_show_overlay(overlay, status) + overlay.visible = status + if status then -- reset colors + -- reload base_color + overlay.base_color = self:get_overlay_base_color(overlay) + overlay.color[1] = overlay.base_color[1] + overlay.color[2] = overlay.base_color[2] + overlay.color[3] = overlay.base_color[3] + overlay.color[4] = overlay.base_color[4] + overlay.opacity = 0 + end +end + + +function RootView:on_mouse_released(button, x, y, ...) if self.dragged_divider then self.dragged_divider = nil end if self.dragged_node then - self.dragged_node = nil + if button == "left" then + if self.dragged_node.dragging then + local node = self.root_node:get_child_overlapping_point(self.mouse.x, self.mouse.y) + local dragged_node = self.dragged_node.node + + if node and not node.locked + -- don't do anything if dragging onto own node, with only one view + and (node ~= dragged_node or #node.views > 1) then + local split_type = node:get_split_type(self.mouse.x, self.mouse.y) + local view = dragged_node.views[self.dragged_node.idx] + + if split_type ~= "middle" and split_type ~= "tab" then -- needs splitting + local new_node = node:split(split_type) + self.root_node:get_node_for_view(view):remove_view(self.root_node, view) + new_node:add_view(view) + elseif split_type == "middle" and node ~= dragged_node then -- move to other node + dragged_node:remove_view(self.root_node, view) + node:add_view(view) + self.root_node:get_node_for_view(view):set_active_view(view) + elseif split_type == "tab" then -- move besides other tabs + local tab_index = node:get_drag_overlay_tab_position(self.mouse.x, self.mouse.y, dragged_node, self.dragged_node.idx) + dragged_node:remove_view(self.root_node, view) + node:add_view(view, tab_index) + self.root_node:get_node_for_view(view):set_active_view(view) + end + self.root_node:update_layout() + core.redraw = true + end + end + self:set_show_overlay(self.drag_overlay, false) + self:set_show_overlay(self.drag_overlay_tab, false) + if self.dragged_node and self.dragged_node.dragging then + core.request_cursor("arrow") + end + self.dragged_node = nil + end + else -- avoid sending on_mouse_released events when dragging tabs + self.root_node:on_mouse_released(button, x, y, ...) end - self.root_node:on_mouse_released(...) end @@ -815,40 +987,33 @@ function RootView:on_mouse_moved(x, y, dx, dy) end self.mouse.x, self.mouse.y = x, y + + local dn = self.dragged_node + if dn and not dn.dragging then + -- start dragging only after enough movement + dn.dragging = common.distance(x, y, dn.drag_start_x, dn.drag_start_y) > style.tab_width * .05 + if dn.dragging then + core.request_cursor("hand") + end + end + + -- avoid sending on_mouse_moved events when dragging tabs + if dn then return end + self.root_node:on_mouse_moved(x, y, dx, dy) - local node = self.root_node:get_child_overlapping_point(x, y) + self.overlapping_node = self.root_node:get_child_overlapping_point(x, y) + local div = self.root_node:get_divider_overlapping_point(x, y) - local tab_index = node and node:get_tab_overlapping_point(x, y) - if node and node:get_scroll_button_index(x, y) then + local tab_index = self.overlapping_node and self.overlapping_node:get_tab_overlapping_point(x, y) + if self.overlapping_node and self.overlapping_node:get_scroll_button_index(x, y) then core.request_cursor("arrow") elseif div then - local axis = (div.type == "hsplit" and "x" or "y") - if div.a:is_resizable(axis) and div.b:is_resizable(axis) then - core.request_cursor(div.type == "hsplit" and "sizeh" or "sizev") - end + core.request_cursor(div.type == "hsplit" and "sizeh" or "sizev") elseif tab_index then core.request_cursor("arrow") - elseif node then - core.request_cursor(node.active_view.cursor) - end - if node and self.dragged_node and (self.dragged_node[1] ~= node or (tab_index and self.dragged_node[2] ~= tab_index)) - and node.type == "leaf" and #node.views > 0 and node.views[1]:is(DocView) then - local tab = self.dragged_node[1].views[self.dragged_node[2]] - if self.dragged_node[1] ~= node then - for i, v in ipairs(node.views) do if v.doc == tab.doc then tab = nil break end end - if tab then - self.dragged_node[1]:remove_view(self.root_node, tab) - node:add_view(tab, tab_index) - self.root_node:update_layout() - self.dragged_node = { node, tab_index or #node.views } - core.redraw = true - end - else - table.remove(self.dragged_node[1].views, self.dragged_node[2]) - table.insert(node.views, tab_index, tab) - self.dragged_node = { node, tab_index } - end + elseif self.overlapping_node then + core.request_cursor(self.overlapping_node.active_view.cursor) end end @@ -856,7 +1021,7 @@ end function RootView:on_mouse_wheel(...) local x, y = self.mouse.x, self.mouse.y local node = self.root_node:get_child_overlapping_point(x, y) - node.active_view:on_mouse_wheel(...) + return node.active_view:on_mouse_wheel(...) end @@ -870,10 +1035,110 @@ function RootView:on_focus_lost(...) core.redraw = true end + +function RootView:interpolate_drag_overlay(overlay) + self:move_towards(overlay, "x", overlay.to.x) + self:move_towards(overlay, "y", overlay.to.y) + self:move_towards(overlay, "w", overlay.to.w) + self:move_towards(overlay, "h", overlay.to.h) + + self:move_towards(overlay, "opacity", overlay.visible and 100 or 0) + overlay.color[4] = overlay.base_color[4] * overlay.opacity / 100 +end + + function RootView:update() copy_position_and_size(self.root_node, self) self.root_node:update() self.root_node:update_layout() + + self:update_drag_overlay() + self:interpolate_drag_overlay(self.drag_overlay) + self:interpolate_drag_overlay(self.drag_overlay_tab) +end + + +function RootView:set_drag_overlay(overlay, x, y, w, h, immediate) + overlay.to.x = x + overlay.to.y = y + overlay.to.w = w + overlay.to.h = h + if immediate then + overlay.x = x + overlay.y = y + overlay.w = w + overlay.h = h + end + if not overlay.visible then + self:set_show_overlay(overlay, true) + end +end + + +local function get_split_sizes(split_type, x, y, w, h) + if split_type == "left" then + w = w * .5 + elseif split_type == "right" then + x = x + w * .5 + w = w * .5 + elseif split_type == "up" then + h = h * .5 + elseif split_type == "down" then + y = y + h * .5 + h = h * .5 + end + return x, y, w, h +end + + +function RootView:update_drag_overlay() + if not (self.dragged_node and self.dragged_node.dragging) then return end + local over = self.root_node:get_child_overlapping_point(self.mouse.x, self.mouse.y) + if over and not over.locked then + local _, _, _, tab_h = over:get_scroll_button_rect(1) + local x, y = over.position.x, over.position.y + local w, h = over.size.x, over.size.y + local split_type = over:get_split_type(self.mouse.x, self.mouse.y) + + if split_type == "tab" and (over ~= self.dragged_node.node or #over.views > 1) then + local tab_index, tab_x, tab_y, tab_w, tab_h = over:get_drag_overlay_tab_position(self.mouse.x, self.mouse.y) + self:set_drag_overlay(self.drag_overlay_tab, + tab_x + (tab_index and 0 or tab_w), tab_y, + style.caret_width, tab_h, + -- avoid showing tab overlay moving between nodes + over ~= self.drag_overlay_tab.last_over) + self:set_show_overlay(self.drag_overlay, false) + self.drag_overlay_tab.last_over = over + else + if (over ~= self.dragged_node.node or #over.views > 1) then + y = y + tab_h + h = h - tab_h + x, y, w, h = get_split_sizes(split_type, x, y, w, h) + end + self:set_drag_overlay(self.drag_overlay, x, y, w, h) + self:set_show_overlay(self.drag_overlay_tab, false) + end + else + self:set_show_overlay(self.drag_overlay, false) + self:set_show_overlay(self.drag_overlay_tab, false) + end +end + + +function RootView:draw_grabbed_tab() + local dn = self.dragged_node + local _,_, w, h = dn.node:get_tab_rect(dn.idx) + local x = self.mouse.x - w / 2 + local y = self.mouse.y - h / 2 + local text = dn.node.views[dn.idx] and dn.node.views[dn.idx]:get_name() or "" + self.root_node:draw_tab(text, true, true, false, x, y, w, h, true) +end + + +function RootView:draw_drag_overlay(ov) + if ov.opacity > 0 then + renderer.draw_rect(ov.x, ov.y, ov.w, ov.h, ov.color) + end end @@ -883,6 +1148,12 @@ function RootView:draw() local t = table.remove(self.deferred_draws) t.fn(table.unpack(t)) end + + self:draw_drag_overlay(self.drag_overlay) + self:draw_drag_overlay(self.drag_overlay_tab) + if self.dragged_node and self.dragged_node.dragging then + self:draw_grabbed_tab() + end if core.cursor_change_req then system.set_cursor(core.cursor_change_req) core.cursor_change_req = nil diff --git a/data/core/start.lua b/data/core/start.lua index 6288b877..31fed147 100644 --- a/data/core/start.lua +++ b/data/core/start.lua @@ -1,6 +1,6 @@ -- this file is used by lite-xl to setup the Lua environment when starting -VERSION = "2.0-beta1" -MOD_VERSION = "1" +VERSION = "@PROJECT_VERSION@" +MOD_VERSION = "2" SCALE = tonumber(os.getenv("LITE_SCALE")) or SCALE PATHSEP = package.config:sub(1, 1) @@ -20,3 +20,14 @@ package.path = DATADIR .. '/?/init.lua;' .. package.path package.path = USERDIR .. '/?.lua;' .. package.path package.path = USERDIR .. '/?/init.lua;' .. package.path +local dynamic_suffix = PLATFORM == "Mac OS X" and 'lib' or (PLATFORM == "Windows" and 'dll' or 'so') +package.cpath = DATADIR .. '/?.' .. dynamic_suffix .. ";" .. USERDIR .. '/?.' .. dynamic_suffix +package.native_plugins = {} +package.searchers = { package.searchers[1], package.searchers[2], function(modname) + local path = package.searchpath(modname, package.cpath) + if not path then return nil end + return system.load_native_plugin, path +end } + +table.pack = table.pack or pack or function(...) return {...} end +table.unpack = table.unpack or unpack diff --git a/data/core/statusview.lua b/data/core/statusview.lua index 58421c31..3342bdb9 100644 --- a/data/core/statusview.lua +++ b/data/core/statusview.lua @@ -6,6 +6,7 @@ local style = require "core.style" local DocView = require "core.docview" local LogView = require "core.logview" local View = require "core.view" +local Object = require "core.object" local StatusView = View:extend() @@ -70,7 +71,7 @@ local function draw_items(self, items, x, y, draw_fn) local color = style.text for _, item in ipairs(items) do - if type(item) == "userdata" then + if Object.is(item, renderer.font) then font = item elseif type(item) == "table" then color = item diff --git a/data/core/style.lua b/data/core/style.lua index 7cf16eb1..3b0d9e35 100644 --- a/data/core/style.lua +++ b/data/core/style.lua @@ -21,11 +21,11 @@ style.tab_width = common.round(170 * SCALE) -- -- On High DPI monitor or non RGB monitor you may consider using antialiasing grayscale instead. -- The antialiasing grayscale with full hinting is interesting for crisp font rendering. -style.font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf", 13 * SCALE) -style.big_font = style.font:copy(40 * SCALE) -style.icon_font = renderer.font.load(DATADIR .. "/fonts/icons.ttf", 14 * SCALE, {antialiasing="grayscale", hinting="full"}) -style.icon_big_font = style.icon_font:copy(20 * SCALE) -style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 13 * SCALE) +style.font = renderer.font.load(DATADIR .. "/fonts/FiraSans-Regular.ttf", 15 * SCALE) +style.big_font = style.font:copy(46 * SCALE) +style.icon_font = renderer.font.load(DATADIR .. "/fonts/icons.ttf", 16 * SCALE, {antialiasing="grayscale", hinting="full"}) +style.icon_big_font = style.icon_font:copy(23 * SCALE) +style.code_font = renderer.font.load(DATADIR .. "/fonts/JetBrainsMono-Regular.ttf", 15 * SCALE) style.background = { common.color "#2e2e32" } style.background2 = { common.color "#252529" } @@ -44,6 +44,8 @@ style.scrollbar2 = { common.color "#4b4b52" } style.nagbar = { common.color "#FF0000" } style.nagbar_text = { common.color "#FFFFFF" } style.nagbar_dim = { common.color "rgba(0, 0, 0, 0.45)" } +style.drag_overlay = { common.color "rgba(255,255,255,0.1)" } +style.drag_overlay_tab = { common.color "#93DDFA" } style.syntax = {} style.syntax["normal"] = { common.color "#e1e1e6" } diff --git a/data/core/tokenizer.lua b/data/core/tokenizer.lua index a20dba5e..bdf6197b 100644 --- a/data/core/tokenizer.lua +++ b/data/core/tokenizer.lua @@ -155,7 +155,7 @@ function tokenizer.tokenize(incoming_syntax, text, state) if count % 2 == 0 then break end end until not res[1] or not close or not target[3] - return unpack(res) + return table.unpack(res) end while i <= #text do diff --git a/data/core/view.lua b/data/core/view.lua index 2fb431d6..d6d1bcbc 100644 --- a/data/core/view.lua +++ b/data/core/view.lua @@ -7,6 +7,10 @@ local Object = require "core.object" local View = Object:extend() +-- context can be "application" or "session". The instance of objects +-- with context "session" will be closed when a project session is +-- terminated. The context "application" is for functional UI elements. +View.context = "application" function View:new() self.position = { x = 0, y = 0 } @@ -98,13 +102,9 @@ function View:on_text_input(text) -- no-op end - function View:on_mouse_wheel(y) - if self.scrollable then - self.scroll.to.y = self.scroll.to.y + y * -config.mouse_wheel_scroll - end -end +end function View:get_content_bounds() local x = self.scroll.x diff --git a/data/plugins/autocomplete.lua b/data/plugins/autocomplete.lua index 867d5360..fde9487e 100644 --- a/data/plugins/autocomplete.lua +++ b/data/plugins/autocomplete.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local common = require "core.common" local config = require "core.config" @@ -8,25 +8,65 @@ local keymap = require "core.keymap" local translate = require "core.doc.translate" local RootView = require "core.rootview" local DocView = require "core.docview" +local Doc = require "core.doc" -config.plugins.autocomplete = { max_suggestions = 6 } +config.plugins.autocomplete = { + -- Amount of characters that need to be written for autocomplete + min_len = 3, + -- The max amount of visible items + max_height = 6, + -- The max amount of scrollable items + max_suggestions = 100, +} local autocomplete = {} -autocomplete.map = {} +autocomplete.map = {} +autocomplete.map_manually = {} +autocomplete.on_close = nil + +-- Flag that indicates if the autocomplete box was manually triggered +-- with the autocomplete.complete() function to prevent the suggestions +-- from getting cluttered with arbitrary document symbols by using the +-- autocomplete.map_manually table. +local triggered_manually = false local mt = { __tostring = function(t) return t.text end } -function autocomplete.add(t) +function autocomplete.add(t, triggered_manually) local items = {} for text, info in pairs(t.items) do - info = (type(info) == "string") and info - table.insert(items, setmetatable({ text = text, info = info }, mt)) + if type(info) == "table" then + table.insert( + items, + setmetatable( + { + text = text, + info = info.info, + desc = info.desc, -- Description shown on item selected + cb = info.cb, -- A callback called once when item is selected + data = info.data -- Optional data that can be used on cb + }, + mt + ) + ) + else + info = (type(info) == "string") and info + table.insert(items, setmetatable({ text = text, info = info }, mt)) + end + end + + if not triggered_manually then + autocomplete.map[t.name] = { files = t.files or ".*", items = items } + else + autocomplete.map_manually[t.name] = { files = t.files or ".*", items = items } end - autocomplete.map[t.name] = { files = t.files or ".*", items = items } end -local max_symbols = config.max_symbols or 2000 +-- +-- Thread that scans open document symbols and cache them +-- +local max_symbols = config.max_symbols core.add_thread(function() local cache = setmetatable({}, { __mode = "k" }) @@ -109,16 +149,39 @@ local last_line, last_col local function reset_suggestions() suggestions_idx = 1 suggestions = {} + + triggered_manually = false + + local doc = core.active_view.doc + if autocomplete.on_close then + autocomplete.on_close(doc, suggestions[suggestions_idx]) + autocomplete.on_close = nil + end end +local function in_table(value, table_array) + for i, element in pairs(table_array) do + if element == value then + return true + end + end + + return false +end local function update_suggestions() local doc = core.active_view.doc local filename = doc and doc.filename or "" + local map = autocomplete.map + + if triggered_manually then + map = autocomplete.map_manually + end + -- get all relevant suggestions for given filename local items = {} - for _, v in pairs(autocomplete.map) do + for _, v in pairs(map) do if common.match_pattern(filename, v.files) then for _, item in pairs(v.items) do table.insert(items, item) @@ -138,7 +201,6 @@ local function update_suggestions() end end - local function get_partial_symbol() local doc = core.active_view.doc local line2, col2 = doc:get_selection() @@ -146,14 +208,12 @@ local function get_partial_symbol() return doc:get_text(line1, col1, line2, col2) end - local function get_active_view() if getmetatable(core.active_view) == DocView then return core.active_view end end - local function get_suggestions_rect(av) if #suggestions == 0 then return 0, 0, 0, 0 @@ -175,15 +235,67 @@ local function get_suggestions_rect(av) max_width = math.max(max_width, w) end + local ah = config.plugins.autocomplete.max_height + + local max_items = #suggestions + if max_items > ah then + max_items = ah + end + + -- additional line to display total items + max_items = max_items + 1 + + if max_width < 150 then + max_width = 150 + end + return x - style.padding.x, y - style.padding.y, max_width + style.padding.x * 2, - #suggestions * (th + style.padding.y) + style.padding.y + max_items * (th + style.padding.y) + style.padding.y end +local function draw_description_box(text, av, sx, sy, sw, sh) + local width = 0 + + local lines = {} + for line in string.gmatch(text.."\n", "(.-)\n") do + width = math.max(width, style.font:get_width(line)) + table.insert(lines, line) + end + + local height = #lines * style.font:get_height() + + -- draw background rect + renderer.draw_rect( + sx + sw + style.padding.x / 4, + sy, + width + style.padding.x * 2, + height + style.padding.y * 2, + style.background3 + ) + + -- draw text + local lh = style.font:get_height() + local y = sy + style.padding.y + local x = sx + sw + style.padding.x / 4 + + for _, line in pairs(lines) do + common.draw_text( + style.font, style.text, line, "left", x + style.padding.x, y, width, lh + ) + y = y + lh + end +end local function draw_suggestions_box(av) + if #suggestions <= 0 then + return + end + + local ah = config.plugins.autocomplete.max_height + -- draw background rect local rx, ry, rw, rh = get_suggestions_rect(av) renderer.draw_rect(rx, ry, rw, rh, style.background3) @@ -192,7 +304,14 @@ local function draw_suggestions_box(av) local font = av:get_font() local lh = font:get_height() + style.padding.y local y = ry + style.padding.y / 2 - for i, s in ipairs(suggestions) do + local show_count = #suggestions <= ah and #suggestions or ah + local start_index = suggestions_idx > ah and (suggestions_idx-(ah-1)) or 1 + + for i=start_index, start_index+show_count-1, 1 do + if not suggestions[i] then + break + end + local s = suggestions[i] local color = (i == suggestions_idx) and style.accent or style.text common.draw_text(font, color, s.text, "left", rx + style.padding.x, y, rw, lh) if s.info then @@ -200,26 +319,55 @@ local function draw_suggestions_box(av) common.draw_text(style.font, color, s.info, "right", rx, y, rw - style.padding.x, lh) end y = y + lh + if suggestions_idx == i then + if s.cb then + s.cb(suggestions_idx, s) + s.cb = nil + s.data = nil + end + if s.desc and #s.desc > 0 then + draw_description_box(s.desc, av, rx, ry, rw, rh) + end + end end + + renderer.draw_rect(rx, y, rw, 2, style.caret) + renderer.draw_rect(rx, y+2, rw, lh, style.background) + common.draw_text( + style.font, + style.accent, + "Items", + "left", + rx + style.padding.x, y, rw, lh + ) + common.draw_text( + style.font, + style.accent, + tostring(suggestions_idx) .. "/" .. tostring(#suggestions), + "right", + rx, y, rw - style.padding.x, lh + ) end - --- patch event logic into RootView -local on_text_input = RootView.on_text_input -local update = RootView.update -local draw = RootView.draw - - -RootView.on_text_input = function(...) - on_text_input(...) - +local function show_autocomplete() local av = get_active_view() if av then -- update partial symbol and suggestions partial = get_partial_symbol() - if #partial >= 3 then + + if #partial >= config.plugins.autocomplete.min_len or triggered_manually then update_suggestions() - last_line, last_col = av.doc:get_selection() + + if not triggered_manually then + last_line, last_col = av.doc:get_selection() + else + local line, col = av.doc:get_selection() + local char = av.doc:get_char(line, col-1, line, col-1) + + if char:match("%s") or (char:match("%p") and col ~= last_col) then + reset_suggestions() + end + end else reset_suggestions() end @@ -233,6 +381,30 @@ RootView.on_text_input = function(...) end end +-- +-- Patch event logic into RootView and Doc +-- +local on_text_input = RootView.on_text_input +local on_text_remove = Doc.remove +local update = RootView.update +local draw = RootView.draw + +RootView.on_text_input = function(...) + on_text_input(...) + show_autocomplete() +end + +Doc.remove = function(self, line1, col1, line2, col2) + on_text_remove(self, line1, col1, line2, col2) + + if triggered_manually and line1 == line2 then + if last_col >= col1 then + reset_suggestions() + else + show_autocomplete() + end + end +end RootView.update = function(...) update(...) @@ -241,13 +413,19 @@ RootView.update = function(...) if av then -- reset suggestions if caret was moved local line, col = av.doc:get_selection() - if line ~= last_line or col ~= last_col then - reset_suggestions() + + if not triggered_manually then + if line ~= last_line or col ~= last_col then + reset_suggestions() + end + else + if line ~= last_line or col < last_col then + reset_suggestions() + end end end end - RootView.draw = function(...) draw(...) @@ -258,12 +436,53 @@ RootView.draw = function(...) end end +-- +-- Public functions +-- +function autocomplete.open(on_close) + triggered_manually = true + if on_close then + autocomplete.on_close = on_close + end + + local av = get_active_view() + last_line, last_col = av.doc:get_selection() + update_suggestions() +end + +function autocomplete.close() + reset_suggestions() +end + +function autocomplete.is_open() + return #suggestions > 0 +end + +function autocomplete.complete(completions, on_close) + reset_suggestions() + + autocomplete.map_manually = {} + autocomplete.add(completions, true) + + autocomplete.open(on_close) +end + +function autocomplete.can_complete() + if #partial >= config.plugins.autocomplete.min_len then + return true + end + return false +end + + +-- +-- Commands +-- local function predicate() return get_active_view() and #suggestions > 0 end - command.add(predicate, { ["autocomplete:complete"] = function() local doc = core.active_view.doc @@ -283,12 +502,19 @@ command.add(predicate, { suggestions_idx = math.min(suggestions_idx + 1, #suggestions) end, + ["autocomplete:cycle"] = function() + local newidx = suggestions_idx + 1 + suggestions_idx = newidx > #suggestions and 1 or newidx + end, + ["autocomplete:cancel"] = function() reset_suggestions() end, }) - +-- +-- Keymaps +-- keymap.add { ["tab"] = "autocomplete:complete", ["up"] = "autocomplete:previous", diff --git a/data/plugins/autoreload.lua b/data/plugins/autoreload.lua index f84d87d6..e772666f 100644 --- a/data/plugins/autoreload.lua +++ b/data/plugins/autoreload.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local config = require "core.config" local Doc = require "core.doc" diff --git a/data/plugins/contextmenu.lua b/data/plugins/contextmenu.lua index c0fe49fd..dc95567f 100644 --- a/data/plugins/contextmenu.lua +++ b/data/plugins/contextmenu.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" @@ -42,6 +42,24 @@ keymap.add { ["menu"] = "context:show" } +local function copy_log() + local item = core.active_view.hovered_item + if item then + system.set_clipboard(core.get_log(item)) + end +end + +local function open_as_doc() + local doc = core.open_doc("logs.txt") + core.root_view:open_doc(doc) + doc:insert(1, 1, core.get_log()) +end + +menu:register("core.logview", { + { text = "Copy entry", command = copy_log }, + { text = "Open as file", command = open_as_doc } +}) + if require("plugins.scale") then menu:register("core.docview", { { text = "Font +", command = "scale:increase" }, diff --git a/data/plugins/detectindent.lua b/data/plugins/detectindent.lua index 9e7ed93c..20541c82 100644 --- a/data/plugins/detectindent.lua +++ b/data/plugins/detectindent.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local command = require "core.command" local common = require "core.common" @@ -102,6 +102,11 @@ end local function update_cache(doc) local type, size, score = detect_indent_stat(doc) local score_threshold = 4 + if score < score_threshold then + -- use default values + type = config.tab_type + size = config.indent_size + end cache[doc] = { type = type, size = size, confirmed = (score >= score_threshold) } doc.indent_info = cache[doc] end @@ -111,20 +116,14 @@ local new = Doc.new function Doc:new(...) new(self, ...) update_cache(self) - if not cache[self].confirmed then - core.add_thread(function () - while not cache[self].confirmed do - update_cache(self) - coroutine.yield(1) - end - end, self) - end end local clean = Doc.clean function Doc:clean(...) clean(self, ...) - update_cache(self) + if not cache[self].confirmed then + update_cache(self) + end end @@ -152,3 +151,78 @@ function DocView:draw(...) return with_indent_override(self.doc, draw, self, ...) end + +local function set_indent_type(doc, type) + cache[doc] = {type = type, + size = cache[doc].value or config.indent_size, + confirmed = true} + doc.indent_info = cache[doc] +end + +local function set_indent_type_command() + core.command_view:enter( + "Specify indent style for this file", + function(value) -- submit + local doc = core.active_view.doc + value = value:lower() + set_indent_type(doc, value == "tabs" and "hard" or "soft") + end, + function(text) -- suggest + return common.fuzzy_match({"tabs", "spaces"}, text) + end, + nil, -- cancel + function(text) -- validate + local t = text:lower() + return t == "tabs" or t == "spaces" + end + ) +end + + +local function set_indent_size(doc, size) + cache[doc] = {type = cache[doc].type or config.tab_type, + size = size, + confirmed = true} + doc.indent_info = cache[doc] +end + +local function set_indent_size_command() + core.command_view:enter( + "Specify indent size for current file", + function(value) -- submit + local value = math.floor(tonumber(value)) + local doc = core.active_view.doc + set_indent_size(doc, value) + end, + nil, -- suggest + nil, -- cancel + function(value) -- validate + local value = tonumber(value) + return value ~= nil and value >= 1 + end + ) +end + + +command.add("core.docview", { + ["indent:set-file-indent-type"] = set_indent_type_command, + ["indent:set-file-indent-size"] = set_indent_size_command +}) + + +command.add(function() + return core.active_view:is(DocView) + and cache[core.active_view.doc] + and cache[core.active_view.doc].type == "soft" + end, { + ["indent:switch-file-to-tabs-indentation"] = function() set_indent_type(core.active_view.doc, "hard") end +}) + + +command.add(function() + return core.active_view:is(DocView) + and cache[core.active_view.doc] + and cache[core.active_view.doc].type == "hard" + end, { + ["indent:switch-file-to-spaces-indentation"] = function() set_indent_type(core.active_view.doc, "soft") end +}) diff --git a/data/plugins/drawwhitespace.lua b/data/plugins/drawwhitespace.lua new file mode 100644 index 00000000..7b7fa011 --- /dev/null +++ b/data/plugins/drawwhitespace.lua @@ -0,0 +1,36 @@ +-- mod-version:2 -- lite-xl 2.0 + +local style = require "core.style" +local DocView = require "core.docview" +local common = require "core.common" + +local draw_line_text = DocView.draw_line_text + +function DocView:draw_line_text(idx, x, y) + local font = (self:get_font() or style.syntax_fonts["comment"]) + local color = style.syntax.comment + local ty = y + self:get_line_text_y_offset() + local tx + local text, offset, s, e = self.doc.lines[idx], 1 + local x1, _, x2, _ = self:get_content_bounds() + local _offset = self:get_x_offset_col(idx, x1) + offset = _offset + while true do + s, e = text:find(" +", offset) + if not s then break end + tx = self:get_col_x_offset(idx, s) + x + renderer.draw_text(font, string.rep("·", e - s + 1), tx, ty, color) + if tx > x + x2 then break end + offset = e + 1 + end + offset = _offset + while true do + s, e = text:find("\t", offset) + if not s then break end + tx = self:get_col_x_offset(idx, s) + x + renderer.draw_text(font, "»", tx, ty, color) + if tx > x + x2 then break end + offset = e + 1 + end + draw_line_text(self, idx, x, y) +end diff --git a/data/plugins/language_c.lua b/data/plugins/language_c.lua index 836e1692..44c3b895 100644 --- a/data/plugins/language_c.lua +++ b/data/plugins/language_c.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local syntax = require "core.syntax" syntax.add { @@ -55,6 +55,17 @@ syntax.add { ["true"] = "literal", ["false"] = "literal", ["NULL"] = "literal", + ["#include"] = "keyword", + ["#if"] = "keyword", + ["#ifdef"] = "keyword", + ["#ifndef"] = "keyword", + ["#else"] = "keyword", + ["#elseif"] = "keyword", + ["#endif"] = "keyword", + ["#define"] = "keyword", + ["#warning"] = "keyword", + ["#error"] = "keyword", + ["#pragma"] = "keyword", }, } diff --git a/data/plugins/language_cpp.lua b/data/plugins/language_cpp.lua index cf3d7cd2..499a09db 100644 --- a/data/plugins/language_cpp.lua +++ b/data/plugins/language_cpp.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 pcall(require, "plugins.language_c") local syntax = require "core.syntax" diff --git a/data/plugins/language_css.lua b/data/plugins/language_css.lua index 08a256f9..222e2f94 100644 --- a/data/plugins/language_css.lua +++ b/data/plugins/language_css.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_html.lua b/data/plugins/language_html.lua index c45b43a3..cebb3f1a 100644 --- a/data/plugins/language_html.lua +++ b/data/plugins/language_html.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_js.lua b/data/plugins/language_js.lua index 671e1bd8..7556b00b 100644 --- a/data/plugins/language_js.lua +++ b/data/plugins/language_js.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_lua.lua b/data/plugins/language_lua.lua index 5df3d29f..165633b6 100644 --- a/data/plugins/language_lua.lua +++ b/data/plugins/language_lua.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/language_md.lua b/data/plugins/language_md.lua index ab2a7d8b..3c1c329a 100644 --- a/data/plugins/language_md.lua +++ b/data/plugins/language_md.lua @@ -1,22 +1,41 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local syntax = require "core.syntax" + + syntax.add { files = { "%.md$", "%.markdown$" }, patterns = { - { pattern = "\\.", type = "normal" }, - { pattern = { "" }, type = "comment" }, - { pattern = { "```", "```" }, type = "string" }, - { pattern = { "``", "``", "\\" }, type = "string" }, - { pattern = { "`", "`", "\\" }, type = "string" }, - { pattern = { "~~", "~~", "\\" }, type = "keyword2" }, - { pattern = "%-%-%-+", type = "comment" }, - { pattern = "%*%s+", type = "operator" }, - { pattern = { "%*", "[%*\n]", "\\" }, type = "operator" }, - { pattern = { "%_", "[%_\n]", "\\" }, type = "keyword2" }, - { pattern = "#.-\n", type = "keyword" }, - { pattern = "!?%[.-%]%(.-%)", type = "function" }, - { pattern = "https?://%S+", type = "function" }, + { pattern = "\\.", type = "normal" }, + { pattern = { "" }, type = "comment" }, + { pattern = { "```c", "```" }, type = "string", syntax = ".c" }, + { pattern = { "```c++", "```" }, type = "string", syntax = ".cpp" }, + { pattern = { "```python", "```" }, type = "string", syntax = ".py" }, + { pattern = { "```ruby", "```" }, type = "string", syntax = ".rb" }, + { pattern = { "```perl", "```" }, type = "string", syntax = ".pl" }, + { pattern = { "```php", "```" }, type = "string", syntax = ".php" }, + { pattern = { "```javascript", "```" }, type = "string", syntax = ".js" }, + { pattern = { "```html", "```" }, type = "string", syntax = ".html" }, + { pattern = { "```xml", "```" }, type = "string", syntax = ".xml" }, + { pattern = { "```css", "```" }, type = "string", syntax = ".css" }, + { pattern = { "```lua", "```" }, type = "string", syntax = ".lua" }, + { pattern = { "```bash", "```" }, type = "string", syntax = ".sh" }, + { pattern = { "```java", "```" }, type = "string", syntax = ".java" }, + { pattern = { "```c#", "```" }, type = "string", syntax = ".cs" }, + { pattern = { "```cmake", "```" }, type = "string", syntax = ".cmake" }, + { pattern = { "```d", "```" }, type = "string", syntax = ".d" }, + { pattern = { "```glsl", "```" }, type = "string", syntax = ".glsl" }, + { pattern = { "```", "```" }, type = "string" }, + { pattern = { "``", "``", "\\" }, type = "string" }, + { pattern = { "`", "`", "\\" }, type = "string" }, + { pattern = { "~~", "~~", "\\" }, type = "keyword2" }, + { pattern = "%-%-%-+", type = "comment" }, + { pattern = "%*%s+", type = "operator" }, + { pattern = { "%*", "[%*\n]", "\\" }, type = "operator" }, + { pattern = { "%_", "[%_\n]", "\\" }, type = "keyword2" }, + { pattern = "#.-\n", type = "keyword" }, + { pattern = "!?%[.-%]%(.-%)", type = "function" }, + { pattern = "https?://%S+", type = "function" }, }, symbols = { }, } diff --git a/data/plugins/language_python.lua b/data/plugins/language_python.lua index 849bafc1..252a0d14 100644 --- a/data/plugins/language_python.lua +++ b/data/plugins/language_python.lua @@ -1,8 +1,8 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local syntax = require "core.syntax" syntax.add { - files = { "%.py$", "%.pyw$" }, + files = { "%.py$", "%.pyw$", "%.rpy$" }, headers = "^#!.*[ /]python", comment = "#", patterns = { diff --git a/data/plugins/language_xml.lua b/data/plugins/language_xml.lua index d97fa9a8..95e310bb 100644 --- a/data/plugins/language_xml.lua +++ b/data/plugins/language_xml.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local syntax = require "core.syntax" syntax.add { diff --git a/data/plugins/lineguide.lua b/data/plugins/lineguide.lua index b838eebb..9f2fca4a 100644 --- a/data/plugins/lineguide.lua +++ b/data/plugins/lineguide.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local config = require "core.config" local style = require "core.style" local DocView = require "core.docview" @@ -6,9 +6,7 @@ local DocView = require "core.docview" local draw_overlay = DocView.draw_overlay function DocView:draw_overlay(...) - local ns = self:get_font():get_width_subpixel("n") * config.line_limit - local ss = self:get_font():subpixel_scale() - local offset = ns / ss + local offset = self:get_font():get_width("n") * config.line_limit local x = self:get_line_screen_position(1) + offset local y = self.position.y local w = math.ceil(SCALE * 1) diff --git a/data/plugins/macro.lua b/data/plugins/macro.lua index 15d8a75e..2678363a 100644 --- a/data/plugins/macro.lua +++ b/data/plugins/macro.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" diff --git a/data/plugins/projectsearch.lua b/data/plugins/projectsearch.lua index 45399ed0..d0d75d7f 100644 --- a/data/plugins/projectsearch.lua +++ b/data/plugins/projectsearch.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local common = require "core.common" local keymap = require "core.keymap" @@ -9,6 +9,7 @@ local View = require "core.view" local ResultsView = View:extend() +ResultsView.context = "session" function ResultsView:new(text, fn) ResultsView.super.new(self) @@ -91,7 +92,7 @@ end function ResultsView:on_mouse_pressed(...) local caught = ResultsView.super.on_mouse_pressed(self, ...) if not caught then - self:open_selected_result() + return self:open_selected_result() end end @@ -107,6 +108,7 @@ function ResultsView:open_selected_result() dv.doc:set_selection(res.line, res.col) dv:scroll_to_line(res.line, false, true) end) + return true end @@ -170,7 +172,7 @@ function ResultsView:draw() local ox, oy = self:get_content_offset() local x, y = ox + style.padding.x, oy + style.padding.y local files_number = core.project_files_number() - local per = files_number and self.last_file_idx / files_number or 1 + local per = common.clamp(files_number and self.last_file_idx / files_number or 1, 0, 1) local text if self.searching then if files_number then diff --git a/data/plugins/quote.lua b/data/plugins/quote.lua index 85a5874c..c714cbf8 100644 --- a/data/plugins/quote.lua +++ b/data/plugins/quote.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local command = require "core.command" local keymap = require "core.keymap" diff --git a/data/plugins/reflow.lua b/data/plugins/reflow.lua index f0051c12..cbaa31ef 100644 --- a/data/plugins/reflow.lua +++ b/data/plugins/reflow.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local config = require "core.config" local command = require "core.command" diff --git a/data/plugins/scale.lua b/data/plugins/scale.lua index 79152f8b..56eabbb0 100644 --- a/data/plugins/scale.lua +++ b/data/plugins/scale.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local common = require "core.common" local command = require "core.command" @@ -13,7 +13,6 @@ config.plugins.scale = { use_mousewheel = true } -local scale_level = 0 local scale_steps = 0.05 local current_scale = SCALE @@ -34,9 +33,6 @@ local function set_scale(scale) local s = scale / current_scale current_scale = scale - -- we set scale_level in case this was called by user - scale_level = (scale - default_scale) / scale_steps - if config.plugins.scale.mode == "ui" then SCALE = scale @@ -48,10 +44,14 @@ local function set_scale(scale) style.tab_width = style.tab_width * s for _, name in ipairs {"font", "big_font", "icon_font", "icon_big_font", "code_font"} do - renderer.font.set_size(style[name], s * style[name]:get_size()) + style[name] = renderer.font.copy(style[name], s * style[name]:get_size()) end else - renderer.font.set_size(style.code_font, s * style.code_font:get_size()) + style.code_font = renderer.font.copy(style.code_font, s * style.code_font:get_size()) + end + + for _, font in pairs(style.syntax_fonts) do + renderer.font.set_size(font, s * font:get_size()) end -- restore scroll positions @@ -67,30 +67,16 @@ local function get_scale() return current_scale end -local on_mouse_wheel = RootView.on_mouse_wheel - -function RootView:on_mouse_wheel(d, ...) - if keymap.modkeys["ctrl"] and config.plugins.scale.use_mousewheel then - if d < 0 then command.perform "scale:decrease" end - if d > 0 then command.perform "scale:increase" end - else - return on_mouse_wheel(self, d, ...) - end -end - local function res_scale() - scale_level = 0 - set_scale(default_scale) + set_scale(default_scale) end local function inc_scale() - scale_level = scale_level + 1 - set_scale(default_scale + scale_level * scale_steps) + set_scale(current_scale + scale_steps) end local function dec_scale() - scale_level = scale_level - 1 - set_scale(default_scale + scale_level * scale_steps) + set_scale(current_scale - scale_steps) end @@ -104,6 +90,8 @@ keymap.add { ["ctrl+0"] = "scale:reset", ["ctrl+-"] = "scale:decrease", ["ctrl+="] = "scale:increase", + ["ctrl+wheelup"] = "scale:increase", + ["ctrl+wheeldown"] = "scale:decrease" } return { diff --git a/data/plugins/tabularize.lua b/data/plugins/tabularize.lua index 2fa06d69..4cdae6ea 100644 --- a/data/plugins/tabularize.lua +++ b/data/plugins/tabularize.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local command = require "core.command" local translate = require "core.doc.translate" diff --git a/data/plugins/toolbarview.lua b/data/plugins/toolbarview.lua index 93102df2..bfd71138 100644 --- a/data/plugins/toolbarview.lua +++ b/data/plugins/toolbarview.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local common = require "core.common" local command = require "core.command" diff --git a/data/plugins/treeview.lua b/data/plugins/treeview.lua index 14ada70f..fa3ab53a 100644 --- a/data/plugins/treeview.lua +++ b/data/plugins/treeview.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local common = require "core.common" local command = require "core.command" @@ -243,7 +243,7 @@ function TreeView:on_mouse_pressed(button, x, y, clicks) end else core.try(function() - local doc_filename = common.relative_path(core.project_dir, hovered_item.abs_filename) + local doc_filename = core.normalize_to_project_dir(hovered_item.abs_filename) core.root_view:open_doc(core.open_doc(doc_filename)) end) end @@ -437,15 +437,31 @@ menu:register( command.add(nil, { ["treeview:toggle"] = function() view.visible = not view.visible - end, + end}) + +command.add(function() return view.hovered_item ~= nil end, { ["treeview:rename"] = function() local old_filename = view.hovered_item.filename + local old_abs_filename = view.hovered_item.abs_filename core.command_view:set_text(old_filename) core.command_view:enter("Rename", function(filename) - os.rename(old_filename, filename) + filename = core.normalize_to_project_dir(filename) + local abs_filename = core.project_absolute_path(filename) + local res, err = os.rename(old_abs_filename, abs_filename) + if res then -- successfully renamed + for _, doc in ipairs(core.docs) do + if doc.abs_filename and old_abs_filename == doc.abs_filename then + doc:set_filename(filename, abs_filename) -- make doc point to the new filename + doc:reset_syntax() + break -- only first needed + end + end + core.log("Renamed \"%s\" to \"%s\"", old_filename, filename) + else + core.error("Error while renaming \"%s\" to \"%s\": %s", old_abs_filename, abs_filename, err) + end core.reschedule_project_scan() - core.log("Renamed \"%s\" to \"%s\"", old_filename, filename) end, common.path_suggest) end, @@ -521,7 +537,7 @@ command.add(nil, { local hovered_item = view.hovered_item if PLATFORM == "Windows" then - system.exec("start " .. hovered_item.abs_filename) + system.exec(string.format("start \"\" %q", hovered_item.abs_filename)) elseif string.find(PLATFORM, "Mac") then system.exec(string.format("open %q", hovered_item.abs_filename)) elseif PLATFORM == "Linux" then diff --git a/data/plugins/trimwhitespace.lua b/data/plugins/trimwhitespace.lua index a6d3d140..79886c67 100644 --- a/data/plugins/trimwhitespace.lua +++ b/data/plugins/trimwhitespace.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local command = require "core.command" local Doc = require "core.doc" diff --git a/data/plugins/workspace.lua b/data/plugins/workspace.lua index 9c1e20c8..1edfbe1e 100644 --- a/data/plugins/workspace.lua +++ b/data/plugins/workspace.lua @@ -1,4 +1,4 @@ --- mod-version:1 -- lite-xl 1.16 +-- mod-version:2 -- lite-xl 2.0 local core = require "core" local common = require "core.common" local DocView = require "core.docview" diff --git a/docs/api/renderer.lua b/docs/api/renderer.lua index bb622131..6820a14d 100644 --- a/docs/api/renderer.lua +++ b/docs/api/renderer.lua @@ -19,6 +19,9 @@ renderer.color = {} ---@class renderer.fontoptions ---@field public antialiasing "'grayscale'" | "'subpixel'" ---@field public hinting "'slight'" | "'none'" | '"full"' +-- @field public bold boolean +-- @field public italic boolean +-- @field public underline boolean renderer.fontoptions = {} --- diff --git a/lib/font_renderer/agg_font_freetype.cpp b/lib/font_renderer/agg_font_freetype.cpp deleted file mode 100644 index c240927e..00000000 --- a/lib/font_renderer/agg_font_freetype.cpp +++ /dev/null @@ -1,1161 +0,0 @@ -//---------------------------------------------------------------------------- -// Anti-Grain Geometry - Version 2.4 -// Copyright (C) 2002-2005 Maxim Shemanarev (http://www.antigrain.com) -// -// Permission to copy, use, modify, sell and distribute this software -// is granted provided this copyright notice appears in all copies. -// This software is provided "as is" without express or implied -// warranty, and with no claim as to its suitability for any purpose. -// -//---------------------------------------------------------------------------- -// Contact: mcseem@antigrain.com -// mcseemagg@yahoo.com -// http://www.antigrain.com -//---------------------------------------------------------------------------- - - -#include -#include "agg_font_freetype.h" -#include "agg_bitset_iterator.h" -#include "agg_renderer_scanline.h" - - -namespace agg -{ - - //------------------------------------------------------------------------------ - // - // This code implements the AUTODIN II polynomial - // The variable corresponding to the macro argument "crc" should - // be an unsigned long. - // Oroginal code by Spencer Garrett - // - - // generated using the AUTODIN II polynomial - // x^32 + x^26 + x^23 + x^22 + x^16 + - // x^12 + x^11 + x^10 + x^8 + x^7 + x^5 + x^4 + x^2 + x^1 + 1 - // - //------------------------------------------------------------------------------ - - static const unsigned crc32tab[256] = - { - 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, - 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3, - 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988, - 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, - 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de, - 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7, - 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, - 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5, - 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172, - 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, - 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940, - 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59, - 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, - 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f, - 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924, - 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, - 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a, - 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433, - 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, - 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01, - 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e, - 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, - 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c, - 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65, - 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, - 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb, - 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0, - 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, - 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086, - 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f, - 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, - 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad, - 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a, - 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, - 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8, - 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1, - 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, - 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7, - 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc, - 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, - 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252, - 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b, - 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, - 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79, - 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236, - 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, - 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04, - 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d, - 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, - 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713, - 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38, - 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, - 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e, - 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777, - 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, - 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45, - 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2, - 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, - 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0, - 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9, - 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, - 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf, - 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94, - 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d, - }; - - - //------------------------------------------------------------------------------ - - static unsigned calc_crc32(const unsigned char* buf, unsigned size) - { - unsigned crc = (unsigned)~0; - const unsigned char* p; - unsigned len = 0; - unsigned nr = size; - - for (len += nr, p = buf; nr--; ++p) - { - crc = (crc >> 8) ^ crc32tab[(crc ^ *p) & 0xff]; - } - return ~crc; - } - - //------------------------------------------------------------------------ - static inline int dbl_to_plain_fx(double d) - { - return int(d * 65536.0); - } - - //------------------------------------------------------------------------ - static inline double int26p6_to_dbl(int p) - { - return double(p) / 64.0; - } - - //------------------------------------------------------------------------ - static inline int dbl_to_int26p6(double p) - { - return int(p * 64.0 + 0.5); - } - - - //------------------------------------------------------------------------ - template - bool decompose_ft_outline(const FT_Outline& outline, - bool flip_y, - const trans_affine& mtx, - PathStorage& path) - { - typedef typename PathStorage::value_type value_type; - - FT_Vector v_last; - FT_Vector v_control; - FT_Vector v_start; - double x1, y1, x2, y2, x3, y3; - - FT_Vector* point; - FT_Vector* limit; - char* tags; - - int n; // index of contour in outline - int first; // index of first point in contour - char tag; // current point's state - - first = 0; - - for(n = 0; n < outline.n_contours; n++) - { - int last; // index of last point in contour - - last = outline.contours[n]; - limit = outline.points + last; - - v_start = outline.points[first]; - v_last = outline.points[last]; - - v_control = v_start; - - point = outline.points + first; - tags = outline.tags + first; - tag = FT_CURVE_TAG(tags[0]); - - // A contour cannot start with a cubic control point! - if(tag == FT_CURVE_TAG_CUBIC) return false; - - // check first point to determine origin - if( tag == FT_CURVE_TAG_CONIC) - { - // first point is conic control. Yes, this happens. - if(FT_CURVE_TAG(outline.tags[last]) == FT_CURVE_TAG_ON) - { - // start at last point if it is on the curve - v_start = v_last; - limit--; - } - else - { - // if both first and last points are conic, - // start at their middle and record its position - // for closure - v_start.x = (v_start.x + v_last.x) / 2; - v_start.y = (v_start.y + v_last.y) / 2; - - v_last = v_start; - } - point--; - tags--; - } - - x1 = int26p6_to_dbl(v_start.x); - y1 = int26p6_to_dbl(v_start.y); - if(flip_y) y1 = -y1; - mtx.transform(&x1, &y1); - path.move_to(value_type(dbl_to_int26p6(x1)), - value_type(dbl_to_int26p6(y1))); - - while(point < limit) - { - point++; - tags++; - - tag = FT_CURVE_TAG(tags[0]); - switch(tag) - { - case FT_CURVE_TAG_ON: // emit a single line_to - { - x1 = int26p6_to_dbl(point->x); - y1 = int26p6_to_dbl(point->y); - if(flip_y) y1 = -y1; - mtx.transform(&x1, &y1); - path.line_to(value_type(dbl_to_int26p6(x1)), - value_type(dbl_to_int26p6(y1))); - //path.line_to(conv(point->x), flip_y ? -conv(point->y) : conv(point->y)); - continue; - } - - case FT_CURVE_TAG_CONIC: // consume conic arcs - { - v_control.x = point->x; - v_control.y = point->y; - - Do_Conic: - if(point < limit) - { - FT_Vector vec; - FT_Vector v_middle; - - point++; - tags++; - tag = FT_CURVE_TAG(tags[0]); - - vec.x = point->x; - vec.y = point->y; - - if(tag == FT_CURVE_TAG_ON) - { - x1 = int26p6_to_dbl(v_control.x); - y1 = int26p6_to_dbl(v_control.y); - x2 = int26p6_to_dbl(vec.x); - y2 = int26p6_to_dbl(vec.y); - if(flip_y) { y1 = -y1; y2 = -y2; } - mtx.transform(&x1, &y1); - mtx.transform(&x2, &y2); - path.curve3(value_type(dbl_to_int26p6(x1)), - value_type(dbl_to_int26p6(y1)), - value_type(dbl_to_int26p6(x2)), - value_type(dbl_to_int26p6(y2))); - continue; - } - - if(tag != FT_CURVE_TAG_CONIC) return false; - - v_middle.x = (v_control.x + vec.x) / 2; - v_middle.y = (v_control.y + vec.y) / 2; - - x1 = int26p6_to_dbl(v_control.x); - y1 = int26p6_to_dbl(v_control.y); - x2 = int26p6_to_dbl(v_middle.x); - y2 = int26p6_to_dbl(v_middle.y); - if(flip_y) { y1 = -y1; y2 = -y2; } - mtx.transform(&x1, &y1); - mtx.transform(&x2, &y2); - path.curve3(value_type(dbl_to_int26p6(x1)), - value_type(dbl_to_int26p6(y1)), - value_type(dbl_to_int26p6(x2)), - value_type(dbl_to_int26p6(y2))); - - //path.curve3(conv(v_control.x), - // flip_y ? -conv(v_control.y) : conv(v_control.y), - // conv(v_middle.x), - // flip_y ? -conv(v_middle.y) : conv(v_middle.y)); - - v_control = vec; - goto Do_Conic; - } - - x1 = int26p6_to_dbl(v_control.x); - y1 = int26p6_to_dbl(v_control.y); - x2 = int26p6_to_dbl(v_start.x); - y2 = int26p6_to_dbl(v_start.y); - if(flip_y) { y1 = -y1; y2 = -y2; } - mtx.transform(&x1, &y1); - mtx.transform(&x2, &y2); - path.curve3(value_type(dbl_to_int26p6(x1)), - value_type(dbl_to_int26p6(y1)), - value_type(dbl_to_int26p6(x2)), - value_type(dbl_to_int26p6(y2))); - - //path.curve3(conv(v_control.x), - // flip_y ? -conv(v_control.y) : conv(v_control.y), - // conv(v_start.x), - // flip_y ? -conv(v_start.y) : conv(v_start.y)); - goto Close; - } - - default: // FT_CURVE_TAG_CUBIC - { - FT_Vector vec1, vec2; - - if(point + 1 > limit || FT_CURVE_TAG(tags[1]) != FT_CURVE_TAG_CUBIC) - { - return false; - } - - vec1.x = point[0].x; - vec1.y = point[0].y; - vec2.x = point[1].x; - vec2.y = point[1].y; - - point += 2; - tags += 2; - - if(point <= limit) - { - FT_Vector vec; - - vec.x = point->x; - vec.y = point->y; - - x1 = int26p6_to_dbl(vec1.x); - y1 = int26p6_to_dbl(vec1.y); - x2 = int26p6_to_dbl(vec2.x); - y2 = int26p6_to_dbl(vec2.y); - x3 = int26p6_to_dbl(vec.x); - y3 = int26p6_to_dbl(vec.y); - if(flip_y) { y1 = -y1; y2 = -y2; y3 = -y3; } - mtx.transform(&x1, &y1); - mtx.transform(&x2, &y2); - mtx.transform(&x3, &y3); - path.curve4(value_type(dbl_to_int26p6(x1)), - value_type(dbl_to_int26p6(y1)), - value_type(dbl_to_int26p6(x2)), - value_type(dbl_to_int26p6(y2)), - value_type(dbl_to_int26p6(x3)), - value_type(dbl_to_int26p6(y3))); - - //path.curve4(conv(vec1.x), - // flip_y ? -conv(vec1.y) : conv(vec1.y), - // conv(vec2.x), - // flip_y ? -conv(vec2.y) : conv(vec2.y), - // conv(vec.x), - // flip_y ? -conv(vec.y) : conv(vec.y)); - continue; - } - - x1 = int26p6_to_dbl(vec1.x); - y1 = int26p6_to_dbl(vec1.y); - x2 = int26p6_to_dbl(vec2.x); - y2 = int26p6_to_dbl(vec2.y); - x3 = int26p6_to_dbl(v_start.x); - y3 = int26p6_to_dbl(v_start.y); - if(flip_y) { y1 = -y1; y2 = -y2; y3 = -y3; } - mtx.transform(&x1, &y1); - mtx.transform(&x2, &y2); - mtx.transform(&x3, &y3); - path.curve4(value_type(dbl_to_int26p6(x1)), - value_type(dbl_to_int26p6(y1)), - value_type(dbl_to_int26p6(x2)), - value_type(dbl_to_int26p6(y2)), - value_type(dbl_to_int26p6(x3)), - value_type(dbl_to_int26p6(y3))); - - //path.curve4(conv(vec1.x), - // flip_y ? -conv(vec1.y) : conv(vec1.y), - // conv(vec2.x), - // flip_y ? -conv(vec2.y) : conv(vec2.y), - // conv(v_start.x), - // flip_y ? -conv(v_start.y) : conv(v_start.y)); - goto Close; - } - } - } - - path.close_polygon(); - - Close: - first = last + 1; - } - - return true; - } - - - - //------------------------------------------------------------------------ - template - void decompose_ft_bitmap_mono(const FT_Bitmap& bitmap, - int x, int y, - bool flip_y, - Scanline& sl, - ScanlineStorage& storage) - { - int i; - const int8u* buf = (const int8u*)bitmap.buffer; - const int bitmap_rows = bitmap.rows, bitmap_width = bitmap.width; - int pitch = bitmap.pitch; - sl.reset(x, x + bitmap.width); - storage.prepare(); - if(flip_y) - { - buf += bitmap.pitch * (bitmap.rows - 1); - y += bitmap.rows; - pitch = -pitch; - } - for(i = 0; i < bitmap_rows; i++) - { - sl.reset_spans(); - bitset_iterator bits(buf, 0); - int j; - for(j = 0; j < bitmap_width; j++) - { - if(bits.bit()) sl.add_cell(x + j, cover_full); - ++bits; - } - buf += pitch; - if(sl.num_spans()) - { - sl.finalize(y - i - 1); - storage.render(sl); - } - } - } - - - - //------------------------------------------------------------------------ - template - void decompose_ft_bitmap_gray8(const FT_Bitmap& bitmap, - int x, int y, - bool flip_y, - Rasterizer& ras, - Scanline& sl, - ScanlineStorage& storage) - { - int i, j; - const int8u* buf = (const int8u*)bitmap.buffer; - const int bitmap_rows = bitmap.rows, bitmap_width = bitmap.width; - int pitch = bitmap.pitch; - sl.reset(x, x + bitmap.width); - storage.prepare(); - if(flip_y) - { - buf += bitmap.pitch * (bitmap.rows - 1); - y += bitmap.rows; - pitch = -pitch; - } - for(i = 0; i < bitmap_rows; i++) - { - sl.reset_spans(); - const int8u* p = buf; - for(j = 0; j < bitmap_width; j++) - { - if(*p) sl.add_cell(x + j, ras.apply_gamma(*p)); - ++p; - } - buf += pitch; - if(sl.num_spans()) - { - sl.finalize(y - i - 1); - storage.render(sl); - } - } - } - - - - - - - - - - - - - - //------------------------------------------------------------------------ - font_engine_freetype_base::~font_engine_freetype_base() - { - unsigned i; - for(i = 0; i < m_num_faces; ++i) - { - delete [] m_face_names[i]; - FT_Done_Face(m_faces[i]); - } - delete [] m_face_names; - delete [] m_faces; - delete [] m_signature; - if(m_library_initialized) FT_Done_FreeType(m_library); - } - - - //------------------------------------------------------------------------ - font_engine_freetype_base::font_engine_freetype_base(bool flag32, - unsigned max_faces) : - m_flag32(flag32), - m_change_stamp(0), - m_last_error(0), - m_name(0), - m_name_len(256-16-1), - m_face_index(0), - m_char_map(FT_ENCODING_NONE), - m_signature(new char [256+256-16]), - m_height(0), - m_width(0), - m_hinting(true), - m_flip_y(false), - m_library_initialized(false), - m_library(0), - m_faces(new FT_Face [max_faces]), - m_face_names(new char* [max_faces]), - m_num_faces(0), - m_max_faces(max_faces), - m_cur_face(0), - m_resolution(0), - m_glyph_rendering(glyph_ren_native_gray8), - m_glyph_index(0), - m_data_size(0), - m_data_type(glyph_data_invalid), - m_bounds(1,1,0,0), - m_advance_x(0.0), - m_advance_y(0.0), - - m_path16(), - m_path32(), - m_curves16(m_path16), - m_curves32(m_path32), - m_scanline_aa(), - m_scanline_bin(), - m_scanlines_aa(), - m_scanlines_bin(), - m_rasterizer() - { - m_curves16.approximation_scale(4.0); - m_curves32.approximation_scale(4.0); - m_last_error = FT_Init_FreeType(&m_library); - if(m_last_error == 0) m_library_initialized = true; - } - - - - //------------------------------------------------------------------------ - void font_engine_freetype_base::resolution(unsigned dpi) - { - m_resolution = dpi; - update_char_size(); - } - - - //------------------------------------------------------------------------ - int font_engine_freetype_base::find_face(const char* face_name) const - { - unsigned i; - for(i = 0; i < m_num_faces; ++i) - { - if(strcmp(face_name, m_face_names[i]) == 0) return i; - } - return -1; - } - - //------------------------------------------------------------------------ - int font_engine_freetype_base::face_height() const - { - return (m_cur_face ? m_cur_face->height : -1); - } - - int font_engine_freetype_base::face_units_em() const - { - return (m_cur_face ? m_cur_face->units_per_EM : -1); - } - - //------------------------------------------------------------------------ - double font_engine_freetype_base::ascender() const - { - if(m_cur_face) - { - return m_cur_face->ascender * height() / m_cur_face->height; - } - return 0.0; - } - - //------------------------------------------------------------------------ - double font_engine_freetype_base::descender() const - { - if(m_cur_face) - { - return m_cur_face->descender * height() / m_cur_face->height; - } - return 0.0; - } - - - //------------------------------------------------------------------------ - bool font_engine_freetype_base::load_font(const char* font_name, - unsigned face_index, - glyph_rendering ren_type, - const char* font_mem, - const long font_mem_size) - { - bool ret = false; - - if(m_library_initialized) - { - m_last_error = 0; - - int idx = find_face(font_name); - if(idx >= 0) - { - m_cur_face = m_faces[idx]; - m_name = m_face_names[idx]; - } - else - { - if(m_num_faces >= m_max_faces) - { - delete [] m_face_names[0]; - FT_Done_Face(m_faces[0]); - memcpy(m_faces, - m_faces + 1, - (m_max_faces - 1) * sizeof(FT_Face)); - memcpy(m_face_names, - m_face_names + 1, - (m_max_faces - 1) * sizeof(char*)); - m_num_faces = m_max_faces - 1; - } - - if (font_mem && font_mem_size) - { - m_last_error = FT_New_Memory_Face(m_library, - (const FT_Byte*)font_mem, - font_mem_size, - face_index, - &m_faces[m_num_faces]); - } - else - { - m_last_error = FT_New_Face(m_library, - font_name, - face_index, - &m_faces[m_num_faces]); - } - - if(m_last_error == 0) - { - m_face_names[m_num_faces] = new char [strlen(font_name) + 1]; - strcpy(m_face_names[m_num_faces], font_name); - m_cur_face = m_faces[m_num_faces]; - m_name = m_face_names[m_num_faces]; - ++m_num_faces; - } - else - { - m_face_names[m_num_faces] = 0; - m_cur_face = 0; - m_name = 0; - } - } - - - if(m_last_error == 0) - { - ret = true; - - switch(ren_type) - { - case glyph_ren_native_mono: - m_glyph_rendering = glyph_ren_native_mono; - break; - - case glyph_ren_native_gray8: - m_glyph_rendering = glyph_ren_native_gray8; - break; - - case glyph_ren_outline: - if(FT_IS_SCALABLE(m_cur_face)) - { - m_glyph_rendering = glyph_ren_outline; - } - else - { - m_glyph_rendering = glyph_ren_native_gray8; - } - break; - - case glyph_ren_agg_mono: - if(FT_IS_SCALABLE(m_cur_face)) - { - m_glyph_rendering = glyph_ren_agg_mono; - } - else - { - m_glyph_rendering = glyph_ren_native_mono; - } - break; - - case glyph_ren_agg_gray8: - if(FT_IS_SCALABLE(m_cur_face)) - { - m_glyph_rendering = glyph_ren_agg_gray8; - } - else - { - m_glyph_rendering = glyph_ren_native_gray8; - } - break; - } - update_signature(); - } - } - return ret; - } - - - //------------------------------------------------------------------------ - bool font_engine_freetype_base::attach(const char* file_name) - { - if(m_cur_face) - { - m_last_error = FT_Attach_File(m_cur_face, file_name); - return m_last_error == 0; - } - return false; - } - - //------------------------------------------------------------------------ - unsigned font_engine_freetype_base::num_faces() const - { - if(m_cur_face) - { - return m_cur_face->num_faces; - } - return 0; - } - - //------------------------------------------------------------------------ - bool font_engine_freetype_base::char_map(FT_Encoding char_map) - { - if(m_cur_face) - { - m_last_error = FT_Select_Charmap(m_cur_face, m_char_map); - if(m_last_error == 0) - { - update_signature(); - return true; - } - } - return false; - } - - //------------------------------------------------------------------------ - bool font_engine_freetype_base::height(double h) - { - m_height = int(h * 64.0); - if(m_cur_face) - { - update_char_size(); - return true; - } - return false; - } - - //------------------------------------------------------------------------ - bool font_engine_freetype_base::width(double w) - { - m_width = int(w * 64.0); - if(m_cur_face) - { - update_char_size(); - return true; - } - return false; - } - - //------------------------------------------------------------------------ - void font_engine_freetype_base::hinting(bool h) - { - m_hinting = h; - if(m_cur_face) - { - update_signature(); - } - } - - //------------------------------------------------------------------------ - void font_engine_freetype_base::flip_y(bool f) - { - m_flip_y = f; - if(m_cur_face) - { - update_signature(); - } - } - - //------------------------------------------------------------------------ - void font_engine_freetype_base::transform(const trans_affine& affine) - { - m_affine = affine; - if(m_cur_face) - { - update_signature(); - } - } - - //------------------------------------------------------------------------ - void font_engine_freetype_base::update_signature() - { - if(m_cur_face && m_name) - { - unsigned name_len = strlen(m_name); - if(name_len > m_name_len) - { - delete [] m_signature; - m_signature = new char [name_len + 32 + 256]; - m_name_len = name_len + 32 - 1; - } - - unsigned gamma_hash = 0; - if(m_glyph_rendering == glyph_ren_native_gray8 || - m_glyph_rendering == glyph_ren_agg_mono || - m_glyph_rendering == glyph_ren_agg_gray8) - { - unsigned char gamma_table[rasterizer_scanline_aa<>::aa_scale]; - unsigned i; - for(i = 0; i < rasterizer_scanline_aa<>::aa_scale; ++i) - { - gamma_table[i] = m_rasterizer.apply_gamma(i); - } - gamma_hash = calc_crc32(gamma_table, sizeof(gamma_table)); - } - - sprintf(m_signature, - "%s,%u,%d,%d,%d:%dx%d,%d,%d,%08X", - m_name, - m_char_map, - m_face_index, - int(m_glyph_rendering), - m_resolution, - m_height, - m_width, - int(m_hinting), - int(m_flip_y), - gamma_hash); - if(m_glyph_rendering == glyph_ren_outline || - m_glyph_rendering == glyph_ren_agg_mono || - m_glyph_rendering == glyph_ren_agg_gray8) - { - double mtx[6]; - char buf[100]; - m_affine.store_to(mtx); - sprintf(buf, ",%08X%08X%08X%08X%08X%08X", - dbl_to_plain_fx(mtx[0]), - dbl_to_plain_fx(mtx[1]), - dbl_to_plain_fx(mtx[2]), - dbl_to_plain_fx(mtx[3]), - dbl_to_plain_fx(mtx[4]), - dbl_to_plain_fx(mtx[5])); - strcat(m_signature, buf); - } - ++m_change_stamp; - } - } - - - //------------------------------------------------------------------------ - void font_engine_freetype_base::update_char_size() - { - if(m_cur_face) - { - if(m_resolution) - { - FT_Set_Char_Size(m_cur_face, - m_width, // char_width in 1/64th of points - m_height, // char_height in 1/64th of points - m_resolution, // horizontal device resolution - m_resolution); // vertical device resolution - } - else - { - FT_Set_Pixel_Sizes(m_cur_face, - m_width >> 6, // pixel_width - m_height >> 6); // pixel_height - } - update_signature(); - } - } - - - - - - //------------------------------------------------------------------------ - bool font_engine_freetype_base::prepare_glyph(unsigned glyph_code) - { - m_glyph_index = FT_Get_Char_Index(m_cur_face, glyph_code); - // For hinting FT_LOAD_DEFAULT could be used but it gives severe - // visual artefacts when scaling fonts x100 along X like - // done by AGG. - m_last_error = FT_Load_Glyph(m_cur_face, - m_glyph_index, - m_hinting ? FT_LOAD_FORCE_AUTOHINT : FT_LOAD_NO_HINTING); - if(m_last_error == 0) - { - switch(m_glyph_rendering) - { - case glyph_ren_native_mono: - m_last_error = FT_Render_Glyph(m_cur_face->glyph, FT_RENDER_MODE_MONO); - if(m_last_error == 0) - { - decompose_ft_bitmap_mono(m_cur_face->glyph->bitmap, - m_cur_face->glyph->bitmap_left, - m_flip_y ? -m_cur_face->glyph->bitmap_top : - m_cur_face->glyph->bitmap_top, - m_flip_y, - m_scanline_bin, - m_scanlines_bin); - m_bounds.x1 = m_scanlines_bin.min_x(); - m_bounds.y1 = m_scanlines_bin.min_y(); - m_bounds.x2 = m_scanlines_bin.max_x() + 1; - m_bounds.y2 = m_scanlines_bin.max_y() + 1; - m_data_size = m_scanlines_bin.byte_size(); - m_data_type = glyph_data_mono; - m_advance_x = int26p6_to_dbl(m_cur_face->glyph->advance.x); - m_advance_y = int26p6_to_dbl(m_cur_face->glyph->advance.y); - return true; - } - break; - - - case glyph_ren_native_gray8: - m_last_error = FT_Render_Glyph(m_cur_face->glyph, FT_RENDER_MODE_NORMAL); - if(m_last_error == 0) - { - decompose_ft_bitmap_gray8(m_cur_face->glyph->bitmap, - m_cur_face->glyph->bitmap_left, - m_flip_y ? -m_cur_face->glyph->bitmap_top : - m_cur_face->glyph->bitmap_top, - m_flip_y, - m_rasterizer, - m_scanline_aa, - m_scanlines_aa); - m_bounds.x1 = m_scanlines_aa.min_x(); - m_bounds.y1 = m_scanlines_aa.min_y(); - m_bounds.x2 = m_scanlines_aa.max_x() + 1; - m_bounds.y2 = m_scanlines_aa.max_y() + 1; - m_data_size = m_scanlines_aa.byte_size(); - m_data_type = glyph_data_gray8; - m_advance_x = int26p6_to_dbl(m_cur_face->glyph->advance.x); - m_advance_y = int26p6_to_dbl(m_cur_face->glyph->advance.y); - return true; - } - break; - - - case glyph_ren_outline: - if(m_last_error == 0) - { - if(m_flag32) - { - m_path32.remove_all(); - if(decompose_ft_outline(m_cur_face->glyph->outline, - m_flip_y, - m_affine, - m_path32)) - { - rect_d bnd = m_path32.bounding_rect(); - m_data_size = m_path32.byte_size(); - m_data_type = glyph_data_outline; - m_bounds.x1 = int(floor(bnd.x1)); - m_bounds.y1 = int(floor(bnd.y1)); - m_bounds.x2 = int(ceil(bnd.x2)); - m_bounds.y2 = int(ceil(bnd.y2)); - m_advance_x = int26p6_to_dbl(m_cur_face->glyph->advance.x); - m_advance_y = int26p6_to_dbl(m_cur_face->glyph->advance.y); - m_affine.transform(&m_advance_x, &m_advance_y); - return true; - } - } - else - { - m_path16.remove_all(); - if(decompose_ft_outline(m_cur_face->glyph->outline, - m_flip_y, - m_affine, - m_path16)) - { - rect_d bnd = m_path16.bounding_rect(); - m_data_size = m_path16.byte_size(); - m_data_type = glyph_data_outline; - m_bounds.x1 = int(floor(bnd.x1)); - m_bounds.y1 = int(floor(bnd.y1)); - m_bounds.x2 = int(ceil(bnd.x2)); - m_bounds.y2 = int(ceil(bnd.y2)); - m_advance_x = int26p6_to_dbl(m_cur_face->glyph->advance.x); - m_advance_y = int26p6_to_dbl(m_cur_face->glyph->advance.y); - m_affine.transform(&m_advance_x, &m_advance_y); - return true; - } - } - } - return false; - - case glyph_ren_agg_mono: - if(m_last_error == 0) - { - m_rasterizer.reset(); - if(m_flag32) - { - m_path32.remove_all(); - decompose_ft_outline(m_cur_face->glyph->outline, - m_flip_y, - m_affine, - m_path32); - m_rasterizer.add_path(m_curves32); - } - else - { - m_path16.remove_all(); - decompose_ft_outline(m_cur_face->glyph->outline, - m_flip_y, - m_affine, - m_path16); - m_rasterizer.add_path(m_curves16); - } - m_scanlines_bin.prepare(); // Remove all - render_scanlines(m_rasterizer, m_scanline_bin, m_scanlines_bin); - m_bounds.x1 = m_scanlines_bin.min_x(); - m_bounds.y1 = m_scanlines_bin.min_y(); - m_bounds.x2 = m_scanlines_bin.max_x() + 1; - m_bounds.y2 = m_scanlines_bin.max_y() + 1; - m_data_size = m_scanlines_bin.byte_size(); - m_data_type = glyph_data_mono; - m_advance_x = int26p6_to_dbl(m_cur_face->glyph->advance.x); - m_advance_y = int26p6_to_dbl(m_cur_face->glyph->advance.y); - m_affine.transform(&m_advance_x, &m_advance_y); - return true; - } - return false; - - - case glyph_ren_agg_gray8: - if(m_last_error == 0) - { - m_rasterizer.reset(); - if(m_flag32) - { - m_path32.remove_all(); - decompose_ft_outline(m_cur_face->glyph->outline, - m_flip_y, - m_affine, - m_path32); - m_rasterizer.add_path(m_curves32); - } - else - { - m_path16.remove_all(); - decompose_ft_outline(m_cur_face->glyph->outline, - m_flip_y, - m_affine, - m_path16); - m_rasterizer.add_path(m_curves16); - } - m_scanlines_aa.prepare(); // Remove all - render_scanlines(m_rasterizer, m_scanline_aa, m_scanlines_aa); - m_bounds.x1 = m_scanlines_aa.min_x(); - m_bounds.y1 = m_scanlines_aa.min_y(); - m_bounds.x2 = m_scanlines_aa.max_x() + 1; - m_bounds.y2 = m_scanlines_aa.max_y() + 1; - m_data_size = m_scanlines_aa.byte_size(); - m_data_type = glyph_data_gray8; - m_advance_x = int26p6_to_dbl(m_cur_face->glyph->advance.x); - m_advance_y = int26p6_to_dbl(m_cur_face->glyph->advance.y); - m_affine.transform(&m_advance_x, &m_advance_y); - return true; - } - return false; - } - } - return false; - } - - - - - //------------------------------------------------------------------------ - void font_engine_freetype_base::write_glyph_to(int8u* data) const - { - if(data && m_data_size) - { - switch(m_data_type) - { - default: return; - case glyph_data_mono: m_scanlines_bin.serialize(data); break; - case glyph_data_gray8: m_scanlines_aa.serialize(data); break; - case glyph_data_outline: - if(m_flag32) - { - m_path32.serialize(data); - } - else - { - m_path16.serialize(data); - } - break; - case glyph_data_invalid: break; - } - } - } - - - - //------------------------------------------------------------------------ - bool font_engine_freetype_base::add_kerning(unsigned first, unsigned second, - double* x, double* y) - { - if(m_cur_face && first && second && FT_HAS_KERNING(m_cur_face)) - { - FT_Vector delta; - FT_Get_Kerning(m_cur_face, first, second, - FT_KERNING_DEFAULT, &delta); - double dx = int26p6_to_dbl(delta.x); - double dy = int26p6_to_dbl(delta.y); - if(m_glyph_rendering == glyph_ren_outline || - m_glyph_rendering == glyph_ren_agg_mono || - m_glyph_rendering == glyph_ren_agg_gray8) - { - m_affine.transform_2x2(&dx, &dy); - } - *x += dx; - *y += dy; - - return true; - } - return false; - } - - - -} - - diff --git a/lib/font_renderer/agg_font_freetype.h b/lib/font_renderer/agg_font_freetype.h deleted file mode 100644 index 6d83999f..00000000 --- a/lib/font_renderer/agg_font_freetype.h +++ /dev/null @@ -1,198 +0,0 @@ -//---------------------------------------------------------------------------- -// Anti-Grain Geometry - Version 2.4 -// Copyright (C) 2002-2005 Maxim Shemanarev (http://www.antigrain.com) -// -// Permission to copy, use, modify, sell and distribute this software -// is granted provided this copyright notice appears in all copies. -// This software is provided "as is" without express or implied -// warranty, and with no claim as to its suitability for any purpose. -// -//---------------------------------------------------------------------------- -// Contact: mcseem@antigrain.com -// mcseemagg@yahoo.com -// http://www.antigrain.com -//---------------------------------------------------------------------------- -// -// See implementation agg_font_freetype.cpp -// -//---------------------------------------------------------------------------- - -#ifndef AGG_FONT_FREETYPE_INCLUDED -#define AGG_FONT_FREETYPE_INCLUDED - -#include -#include FT_FREETYPE_H - - -#include "agg_scanline_storage_aa.h" -#include "agg_scanline_storage_bin.h" -#include "agg_scanline_u.h" -#include "agg_scanline_bin.h" -#include "agg_path_storage_integer.h" -#include "agg_rasterizer_scanline_aa.h" -#include "agg_conv_curve.h" -#include "agg_font_cache_manager.h" -#include "agg_trans_affine.h" - -namespace agg -{ - - - //-----------------------------------------------font_engine_freetype_base - class font_engine_freetype_base - { - public: - //-------------------------------------------------------------------- - typedef serialized_scanlines_adaptor_aa gray8_adaptor_type; - typedef serialized_scanlines_adaptor_bin mono_adaptor_type; - typedef scanline_storage_aa8 scanlines_aa_type; - typedef scanline_storage_bin scanlines_bin_type; - - //-------------------------------------------------------------------- - ~font_engine_freetype_base(); - font_engine_freetype_base(bool flag32, unsigned max_faces = 32); - - // Set font parameters - //-------------------------------------------------------------------- - void resolution(unsigned dpi); - bool load_font(const char* font_name, unsigned face_index, glyph_rendering ren_type, - const char* font_mem = 0, const long font_mem_size = 0); - bool attach(const char* file_name); - bool char_map(FT_Encoding map); - bool height(double h); - bool width(double w); - void hinting(bool h); - void flip_y(bool f); - void transform(const trans_affine& affine); - - // Set Gamma - //-------------------------------------------------------------------- - template void gamma(const GammaF& f) - { - m_rasterizer.gamma(f); - } - - // Accessors - //-------------------------------------------------------------------- - int last_error() const { return m_last_error; } - unsigned resolution() const { return m_resolution; } - const char* name() const { return m_name; } - unsigned num_faces() const; - FT_Encoding char_map() const { return m_char_map; } - double height() const { return double(m_height) / 64.0; } - double width() const { return double(m_width) / 64.0; } - double ascender() const; - double descender() const; - int face_height() const; - int face_units_em()const; - bool hinting() const { return m_hinting; } - bool flip_y() const { return m_flip_y; } - - - // Interface mandatory to implement for font_cache_manager - //-------------------------------------------------------------------- - const char* font_signature() const { return m_signature; } - int change_stamp() const { return m_change_stamp; } - - bool prepare_glyph(unsigned glyph_code); - unsigned glyph_index() const { return m_glyph_index; } - unsigned data_size() const { return m_data_size; } - glyph_data_type data_type() const { return m_data_type; } - const rect_i& bounds() const { return m_bounds; } - double advance_x() const { return m_advance_x; } - double advance_y() const { return m_advance_y; } - void write_glyph_to(int8u* data) const; - bool add_kerning(unsigned first, unsigned second, - double* x, double* y); - - private: - font_engine_freetype_base(const font_engine_freetype_base&); - const font_engine_freetype_base& operator = (const font_engine_freetype_base&); - - void update_char_size(); - void update_signature(); - int find_face(const char* face_name) const; - - bool m_flag32; - int m_change_stamp; - int m_last_error; - char* m_name; - unsigned m_name_len; - unsigned m_face_index; - FT_Encoding m_char_map; - char* m_signature; - unsigned m_height; - unsigned m_width; - bool m_hinting; - bool m_flip_y; - bool m_library_initialized; - FT_Library m_library; // handle to library - FT_Face* m_faces; // A pool of font faces - char** m_face_names; - unsigned m_num_faces; - unsigned m_max_faces; - FT_Face m_cur_face; // handle to the current face object - int m_resolution; - glyph_rendering m_glyph_rendering; - unsigned m_glyph_index; - unsigned m_data_size; - glyph_data_type m_data_type; - rect_i m_bounds; - double m_advance_x; - double m_advance_y; - trans_affine m_affine; - - path_storage_integer m_path16; - path_storage_integer m_path32; - conv_curve > m_curves16; - conv_curve > m_curves32; - scanline_u8 m_scanline_aa; - scanline_bin m_scanline_bin; - scanlines_aa_type m_scanlines_aa; - scanlines_bin_type m_scanlines_bin; - rasterizer_scanline_aa<> m_rasterizer; - }; - - - - - //------------------------------------------------font_engine_freetype_int16 - // This class uses values of type int16 (10.6 format) for the vector cache. - // The vector cache is compact, but when rendering glyphs of height - // more that 200 there integer overflow can occur. - // - class font_engine_freetype_int16 : public font_engine_freetype_base - { - public: - typedef serialized_integer_path_adaptor path_adaptor_type; - typedef font_engine_freetype_base::gray8_adaptor_type gray8_adaptor_type; - typedef font_engine_freetype_base::mono_adaptor_type mono_adaptor_type; - typedef font_engine_freetype_base::scanlines_aa_type scanlines_aa_type; - typedef font_engine_freetype_base::scanlines_bin_type scanlines_bin_type; - - font_engine_freetype_int16(unsigned max_faces = 32) : - font_engine_freetype_base(false, max_faces) {} - }; - - //------------------------------------------------font_engine_freetype_int32 - // This class uses values of type int32 (26.6 format) for the vector cache. - // The vector cache is twice larger than in font_engine_freetype_int16, - // but it allows you to render glyphs of very large sizes. - // - class font_engine_freetype_int32 : public font_engine_freetype_base - { - public: - typedef serialized_integer_path_adaptor path_adaptor_type; - typedef font_engine_freetype_base::gray8_adaptor_type gray8_adaptor_type; - typedef font_engine_freetype_base::mono_adaptor_type mono_adaptor_type; - typedef font_engine_freetype_base::scanlines_aa_type scanlines_aa_type; - typedef font_engine_freetype_base::scanlines_bin_type scanlines_bin_type; - - font_engine_freetype_int32(unsigned max_faces = 32) : - font_engine_freetype_base(true, max_faces) {} - }; - - -} - -#endif diff --git a/lib/font_renderer/agg_lcd_distribution_lut.h b/lib/font_renderer/agg_lcd_distribution_lut.h deleted file mode 100644 index 5e305d70..00000000 --- a/lib/font_renderer/agg_lcd_distribution_lut.h +++ /dev/null @@ -1,73 +0,0 @@ -// Adapted by Francesco Abbate for GSL Shell -// Original code's copyright below. -//---------------------------------------------------------------------------- -// Anti-Grain Geometry - Version 2.4 -// Copyright (C) 2002-2005 Maxim Shemanarev (http://www.antigrain.com) -// -// Permission to copy, use, modify, sell and distribute this software -// is granted provided this copyright notice appears in all copies. -// This software is provided "as is" without express or implied -// warranty, and with no claim as to its suitability for any purpose. -// -//---------------------------------------------------------------------------- -// Contact: mcseem@antigrain.com -// mcseemagg@yahoo.com -// http://www.antigrain.com -//---------------------------------------------------------------------------- - -#ifndef AGG_LCD_DISTRIBUTION_LUT_INCLUDED -#define AGG_LCD_DISTRIBUTION_LUT_INCLUDED - -#include - -#include "agg_basics.h" - -namespace agg -{ - - //=====================================================lcd_distribution_lut - class lcd_distribution_lut - { - public: - lcd_distribution_lut(double prim, double second, double tert) - { - double norm = 1.0 / (prim + second*2 + tert*2); - prim *= norm; - second *= norm; - tert *= norm; - for(unsigned i = 0; i < 256; i++) - { - unsigned b = (i << 8); - unsigned s = round(second * b); - unsigned t = round(tert * b); - unsigned p = b - (2*s + 2*t); - - m_data[3*i + 1] = s; /* secondary */ - m_data[3*i + 2] = t; /* tertiary */ - m_data[3*i ] = p; /* primary */ - } - } - - unsigned convolution(const int8u* covers, int i0, int i_min, int i_max) const - { - unsigned sum = 0; - int k_min = (i0 >= i_min + 2 ? -2 : i_min - i0); - int k_max = (i0 <= i_max - 2 ? 2 : i_max - i0); - for (int k = k_min; k <= k_max; k++) - { - /* select the primary, secondary or tertiary channel */ - int channel = std::abs(k) % 3; - int8u c = covers[i0 + k]; - sum += m_data[3*c + channel]; - } - - return (sum + 128) >> 8; - } - - private: - unsigned short m_data[256*3]; - }; - -} - -#endif diff --git a/lib/font_renderer/agg_pixfmt_alpha8.h b/lib/font_renderer/agg_pixfmt_alpha8.h deleted file mode 100644 index f91378fe..00000000 --- a/lib/font_renderer/agg_pixfmt_alpha8.h +++ /dev/null @@ -1,93 +0,0 @@ -#pragma once - -#include -#include "agg_basics.h" -#include "agg_rendering_buffer.h" - -namespace agg -{ - // This is a special purpose color type that only has the alpha channel. - // It can be thought as a gray color but with the intensity always on. - // It is actually used to store coverage information. - struct alpha8 - { - typedef int8u value_type; - typedef int32u calc_type; - typedef int32 long_type; - enum base_scale_e - { - base_shift = 8, - base_scale = 1 << base_shift, - base_mask = base_scale - 1 - }; - - value_type a; - - //-------------------------------------------------------------------- - alpha8(unsigned a_=base_mask) : - a(int8u(a_)) {} - }; - - // Pixer format to store coverage information. - class pixfmt_alpha8 - { - public: - typedef alpha8 color_type; - typedef int8u value_type; - typedef int32u calc_type; - typedef agg::rendering_buffer::row_data row_data; - - //-------------------------------------------------------------------- - pixfmt_alpha8(rendering_buffer& rb): m_rbuf(&rb) - { - } - - //-------------------------------------------------------------------- - unsigned width() const { - return m_rbuf->width(); - } - unsigned height() const { - return m_rbuf->height(); - } - - // This method should never be called when using the scanline_u8. - // The use of scanline_p8 should be avoided because if does not works - // properly for rendering fonts because single hspan are split in many - // hline/hspan elements and pixel whitening happens. - void blend_hline(int x, int y, unsigned len, - const color_type& c, int8u cover) - { } - - void copy_hline(int x, int y, unsigned len, const color_type& c) - { - value_type* p = (value_type*) m_rbuf->row_ptr(y) + x; - do - { - *p = c.a; - p++; - } - while(--len); - } - - //-------------------------------------------------------------------- - void blend_solid_hspan(int x, int y, - unsigned len, - const color_type& c, - const int8u* covers) - { - value_type* p = (value_type*) m_rbuf->row_ptr(y) + x; - do - { - calc_type alpha = (calc_type(c.a) * (calc_type(*covers) + 1)) >> 8; - *p = alpha; - p++; - ++covers; - } - while(--len); - } - - private: - rendering_buffer* m_rbuf; - }; - -} diff --git a/lib/font_renderer/font_renderer.cpp b/lib/font_renderer/font_renderer.cpp deleted file mode 100644 index 8026a89d..00000000 --- a/lib/font_renderer/font_renderer.cpp +++ /dev/null @@ -1,445 +0,0 @@ -#include "font_renderer.h" - -#include "agg_lcd_distribution_lut.h" -#include "agg_pixfmt_rgb.h" -#include "agg_pixfmt_rgba.h" - -#include "font_renderer_alpha.h" - -// Important: when a subpixel scale is used the width below will be the width in logical pixel. -// As each logical pixel contains 3 subpixels it means that the 'pixels' pointer -// will hold enough space for '3 * width' uint8_t values. -struct FR_Bitmap { - agg::int8u *pixels; - int width, height; -}; - -class FR_Renderer { -public: - // Conventional LUT values: (1./3., 2./9., 1./9.) - // The values below are fine tuned as in the Elementary Plot library. - - FR_Renderer(bool hinting, bool kerning, bool subpixel, bool prescale_x) : - m_renderer(hinting, kerning, subpixel, prescale_x), - m_lcd_lut(0.448, 0.184, 0.092), - m_subpixel(subpixel) - { } - - font_renderer_alpha& renderer_alpha() { return m_renderer; } - agg::lcd_distribution_lut& lcd_distribution_lut() { return m_lcd_lut; } - int subpixel_scale() const { return (m_subpixel ? 3 : 1); } - -private: - font_renderer_alpha m_renderer; - agg::lcd_distribution_lut m_lcd_lut; - int m_subpixel; -}; - -FR_Renderer *FR_Renderer_New(unsigned int flags) { - bool hinting = ((flags & FR_HINTING) != 0); - bool kerning = ((flags & FR_KERNING) != 0); - bool subpixel = ((flags & FR_SUBPIXEL) != 0); - bool prescale_x = ((flags & FR_PRESCALE_X) != 0); - return new FR_Renderer(hinting, kerning, subpixel, prescale_x); -} - -FR_Bitmap* FR_Bitmap_New(FR_Renderer *font_renderer, int width, int height) { - const int subpixel_scale = font_renderer->subpixel_scale(); - FR_Bitmap *image = (FR_Bitmap *) malloc(sizeof(FR_Bitmap) + width * height * subpixel_scale); - if (!image) { return NULL; } - image->pixels = (agg::int8u *) (image + 1); - image->width = width; - image->height = height; - return image; -} - -void FR_Bitmap_Free(FR_Bitmap *image) { - free(image); -} - -void FR_Renderer_Free(FR_Renderer *font_renderer) { - delete font_renderer; -} - -int FR_Subpixel_Scale(FR_Renderer *font_renderer) { - return font_renderer->subpixel_scale(); -} - -int FR_Load_Font(FR_Renderer *font_renderer, const char *filename) { - bool success = font_renderer->renderer_alpha().load_font(filename); - return (success ? 0 : 1); -} - -int FR_Get_Font_Height(FR_Renderer *font_renderer, float size) { - font_renderer_alpha& renderer_alpha = font_renderer->renderer_alpha(); - double ascender, descender; - renderer_alpha.get_font_vmetrics(ascender, descender); - int face_height = renderer_alpha.get_face_height(); - float scale = renderer_alpha.scale_for_em_to_pixels(size); - return int((ascender - descender) * face_height * scale + 0.5); -} - -static void glyph_trim_rect(agg::rendering_buffer& ren_buf, FR_Bitmap_Glyph_Metrics& gli, int subpixel_scale) { - const int height = ren_buf.height(); - int x0 = gli.x0 * subpixel_scale, x1 = gli.x1 * subpixel_scale; - int y0 = gli.y0, y1 = gli.y1; - for (int y = gli.y0; y < gli.y1; y++) { - const uint8_t *row = ren_buf.row_ptr(height - 1 - y); - unsigned int row_bitsum = 0; - for (int x = x0; x < x1; x++) { - row_bitsum |= row[x]; - } - if (row_bitsum == 0) { - y0++; - } else { - break; - } - } - for (int y = gli.y1 - 1; y >= y0; y--) { - const uint8_t *row = ren_buf.row_ptr(height - 1 - y); - unsigned int row_bitsum = 0; - for (int x = x0; x < x1; x++) { - row_bitsum |= row[x]; - } - if (row_bitsum == 0) { - y1--; - } else { - break; - } - } - for (int x = gli.x0 * subpixel_scale; x < gli.x1 * subpixel_scale; x += subpixel_scale) { - unsigned int xaccu = 0; - for (int y = y0; y < y1; y++) { - const uint8_t *row = ren_buf.row_ptr(height - 1 - y); - for (int i = 0; i < subpixel_scale; i++) { - xaccu |= row[x + i]; - } - } - if (xaccu == 0) { - x0 += subpixel_scale; - } else { - break; - } - } - for (int x = (gli.x1 - 1) * subpixel_scale; x >= x0; x -= subpixel_scale) { - unsigned int xaccu = 0; - for (int y = y0; y < y1; y++) { - const uint8_t *row = ren_buf.row_ptr(height - 1 - y); - for (int i = 0; i < subpixel_scale; i++) { - xaccu |= row[x + i]; - } - } - if (xaccu == 0) { - x1 -= subpixel_scale; - } else { - break; - } - } - gli.xoff += (x0 / subpixel_scale) - gli.x0; - gli.yoff += (y0 - gli.y0); - gli.x0 = x0 / subpixel_scale; - gli.y0 = y0; - gli.x1 = x1 / subpixel_scale; - gli.y1 = y1; -} - -static void glyph_lut_convolution(agg::rendering_buffer ren_buf, agg::lcd_distribution_lut& lcd_lut, agg::int8u *covers_buf, FR_Bitmap_Glyph_Metrics& gli) { - const int subpixel = 3; - const int x0 = gli.x0, y0 = gli.y0, x1 = gli.x1, y1 = gli.y1; - const int len = (x1 - x0) * subpixel; - const int height = ren_buf.height(); - for (int y = y0; y < y1; y++) { - agg::int8u *covers = ren_buf.row_ptr(height - 1 - y) + x0 * subpixel; - memcpy(covers_buf, covers, len); - for (int x = x0 - 1; x < x1 + 1; x++) { - for (int i = 0; i < subpixel; i++) { - const int cx = (x - x0) * subpixel + i; - covers[cx] = lcd_lut.convolution(covers_buf, cx, 0, len - 1); - } - } - } - gli.x0 -= 1; - gli.x1 += 1; - gli.xoff -= 1; -} - -// The two functions below are needed because in C and C++ integer division -// is rounded toward zero. - -// euclidean division rounded toward positive infinite -static int div_pos(int n, int p) { - return n >= 0 ? (n + p - 1) / p : (n / p); -} - -// euclidean division rounded toward negative infinite -static int div_neg(int n, int p) { - return n >= 0 ? (n / p) : ((n - p + 1) / p); -} - -FR_Bitmap *FR_Bake_Font_Bitmap(FR_Renderer *font_renderer, int font_height, - int first_char, int num_chars, FR_Bitmap_Glyph_Metrics *glyphs) -{ - font_renderer_alpha& renderer_alpha = font_renderer->renderer_alpha(); - agg::lcd_distribution_lut& lcd_lut = font_renderer->lcd_distribution_lut(); - const int subpixel_scale = font_renderer->subpixel_scale(); - - double ascender, descender; - renderer_alpha.get_font_vmetrics(ascender, descender); - const int ascender_px = int(ascender * font_height); - const int pad_y = 1; - - // When using subpixel font rendering it is needed to leave a padding pixel on the left and on the right. - // Since each pixel is composed by n subpixel we set below x_start to subpixel_scale instead than zero. - // In addition we need one more pixel on the left because of subpixel positioning so - // it adds up to 2 * subpixel_scale. - // Note about the coordinates: they are AGG-like so x is positive toward the right and - // y is positive in the upper direction. - const int x_start = 2 * subpixel_scale; - const agg::alpha8 text_color(0xff); -#ifdef FONT_RENDERER_HEIGHT_HACK - const int font_height_reduced = (font_height * 86) / 100; -#else - const int font_height_reduced = font_height; -#endif - renderer_alpha.set_font_height(font_height_reduced); - - int *index = (int *) malloc(num_chars * sizeof(int)); - agg::rect_i *bounds = (agg::rect_i *) malloc(num_chars * sizeof(agg::rect_i)); - if (!index || !bounds) { - free(index); - free(bounds); - return NULL; - } - - int x_size_sum = 0, glyph_count = 0; - for (int i = 0; i < num_chars; i++) { - int codepoint = first_char + i; - index[i] = i; - if (renderer_alpha.codepoint_bounds(codepoint, subpixel_scale, bounds[i])) { - // Invalid glyph - bounds[i].x1 = 0; - bounds[i].y1 = 0; - bounds[i].x2 = -1; - bounds[i].y2 = -1; - } else { - if (bounds[i].x2 > bounds[i].x1) { - x_size_sum += bounds[i].x2 - bounds[i].x1; - glyph_count++; - } - bounds[i].x1 = subpixel_scale * div_neg(bounds[i].x1, subpixel_scale); - bounds[i].x2 = subpixel_scale * div_pos(bounds[i].x2, subpixel_scale); - } - } - - // Simple insertion sort algorithm: https://en.wikipedia.org/wiki/Insertion_sort - int i = 1; - while (i < num_chars) { - int j = i; - while (j > 0 && bounds[index[j-1]].y2 - bounds[index[j-1]].y1 > bounds[index[j]].y2 - bounds[index[j]].y1) { - int tmp = index[j]; - index[j] = index[j-1]; - index[j-1] = tmp; - j = j - 1; - } - i = i + 1; - } - - const int glyph_avg_width = glyph_count > 0 ? x_size_sum / (glyph_count * subpixel_scale) : font_height; - const int pixels_width = glyph_avg_width * 28; - - // dry run simulating pixel position to estimate required image's height - int x = x_start, y = 0, y_bottom = y; - for (int i = 0; i < num_chars; i++) { - const agg::rect_i& gbounds = bounds[index[i]]; - if (gbounds.x2 < gbounds.x1) continue; - // 1. It is very important to ensure that the x's increment below (1) and in - // (2), (3) and (4) are perfectly the same. - // Note that x_step below is always an integer multiple of subpixel_scale. - const int x_step = gbounds.x2 + 3 * subpixel_scale; - if (x + x_step >= pixels_width * subpixel_scale) { - x = x_start; - y = y_bottom; - } - // 5. Ensure that y's increment below is exactly the same to the one used in (6) - const int glyph_y_bottom = y - 2 * pad_y - (gbounds.y2 - gbounds.y1); - y_bottom = (y_bottom > glyph_y_bottom ? glyph_y_bottom : y_bottom); - // 2. Ensure x's increment is aligned with (1) - x = x + x_step; - } - - agg::int8u *cover_swap_buffer = (agg::int8u *) malloc(sizeof(agg::int8u) * (pixels_width * subpixel_scale)); - if (!cover_swap_buffer) { - free(index); - free(bounds); - return NULL; - } - - const int pixels_height = -y_bottom + 1; - const int pixel_size = 1; - FR_Bitmap *image = FR_Bitmap_New(font_renderer, pixels_width, pixels_height); - if (!image) { - free(index); - free(bounds); - free(cover_swap_buffer); - return NULL; - } - - agg::int8u *pixels = image->pixels; - memset(pixels, 0x00, pixels_width * pixels_height * subpixel_scale * pixel_size); - agg::rendering_buffer ren_buf(pixels, pixels_width * subpixel_scale, pixels_height, -pixels_width * subpixel_scale * pixel_size); - - // The variable y_bottom will be used to go down to the next row by taking into - // account the space occupied by each glyph of the current row along the y direction. - x = x_start; - // Set y to the image's height minus one to begin writing glyphs in the upper part of the image. - y = pixels_height - 1; - y_bottom = y; - for (int i = 0; i < num_chars; i++) { - // Important: the variable x in this loop should always be an integer multiple - // of subpixel_scale. - int codepoint = first_char + index[i]; - const agg::rect_i& gbounds = bounds[index[i]]; - if (gbounds.x2 < gbounds.x1) continue; - - // 3. Ensure x's increment is aligned with (1) - // Note that x_step below is always an integer multiple of subpixel_scale. - // We need 3 * subpixel_scale because: - // . +1 pixel on the left, because of RGB color filter - // . +1 pixel on the right, because of RGB color filter - // . +1 pixel on the right, because of subpixel positioning - // and each pixel requires "subpixel_scale" sub-pixels. - const int x_step = gbounds.x2 + 3 * subpixel_scale; - if (x + x_step >= pixels_width * subpixel_scale) { - // No more space along x, begin writing the row below. - x = x_start; - y = y_bottom; - } - - const int y_baseline = y - pad_y - gbounds.y2; - // 6. Ensure the y's increment below is aligned with the increment used in (5) - const int glyph_y_bottom = y - 2 * pad_y - (gbounds.y2 - gbounds.y1); - y_bottom = (y_bottom > glyph_y_bottom ? glyph_y_bottom : y_bottom); - - double x_next = x, y_next = y_baseline; - renderer_alpha.render_codepoint(ren_buf, text_color, x_next, y_next, codepoint, subpixel_scale); - - // The y coordinate for the glyph below is positive in the bottom direction, - // like is used by Lite's drawing system. - FR_Bitmap_Glyph_Metrics& glyph_info = glyphs[index[i]]; - glyph_info.x0 = x / subpixel_scale; - glyph_info.y0 = pixels_height - 1 - (y_baseline + gbounds.y2 + pad_y); - glyph_info.x1 = div_pos(x_next + 0.5, subpixel_scale); - glyph_info.y1 = pixels_height - 1 - (y_baseline + gbounds.y1 - pad_y); - - glyph_info.xoff = 0; - glyph_info.yoff = -pad_y - gbounds.y2 + ascender_px; - // Note that below the xadvance is in pixels times the subpixel_scale. - // This is meant for subpixel positioning. - glyph_info.xadvance = roundf(x_next - x); - - if (subpixel_scale != 1 && glyph_info.x1 > glyph_info.x0) { - glyph_lut_convolution(ren_buf, lcd_lut, cover_swap_buffer, glyph_info); - } - glyph_trim_rect(ren_buf, glyph_info, subpixel_scale); - - // When subpixel is activated we need one padding pixel on the left and on the right - // and one more because of subpixel positioning. - // 4. Ensure x's increment is aligned with (1) - x = x + x_step; - } - - free(index); - free(bounds); - free(cover_swap_buffer); - return image; -} - -template -void blend_solid_hspan(agg::rendering_buffer& rbuf, int x, int y, unsigned len, - const agg::rgba8& c, const agg::int8u* covers) -{ - const int pixel_size = 4; - agg::int8u* p = rbuf.row_ptr(y) + x * pixel_size; - do - { - const unsigned alpha = *covers; - const unsigned r = p[Order::R], g = p[Order::G], b = p[Order::B]; - p[Order::R] = (((unsigned(c.r) - r) * alpha) >> 8) + r; - p[Order::G] = (((unsigned(c.g) - g) * alpha) >> 8) + g; - p[Order::B] = (((unsigned(c.b) - b) * alpha) >> 8) + b; - // Leave p[3], the alpha channel value unmodified. - p += 4; - ++covers; - } - while(--len); -} - -template -void blend_solid_hspan_subpixel(agg::rendering_buffer& rbuf, agg::lcd_distribution_lut& lcd_lut, - const int x, const int y, unsigned len, - const agg::rgba8& c, - const agg::int8u* covers) -{ - const int pixel_size = 4; - const unsigned rgb[3] = { c.r, c.g, c.b }; - agg::int8u* p = rbuf.row_ptr(y) + x * pixel_size; - - // Indexes to adress RGB colors in a BGRA32 format. - const int pixel_index[3] = {Order::R, Order::G, Order::B}; - for (unsigned cx = 0; cx < len; cx += 3) - { - for (int i = 0; i < 3; i++) { - const unsigned cover_value = covers[cx + i]; - const unsigned alpha = (cover_value + 1) * (c.a + 1); - const unsigned src_col = *(p + pixel_index[i]); - *(p + pixel_index[i]) = (((rgb[i] - src_col) * alpha) + (src_col << 16)) >> 16; - } - // Leave p[3], the alpha channel value unmodified. - p += 4; - } -} - -// destination implicitly BGRA32. Source implictly single-byte renderer_alpha coverage with subpixel scale = 3. -// FIXME: consider using something like RenColor* instead of uint8_t * for dst. -void FR_Blend_Glyph(FR_Renderer *font_renderer, FR_Clip_Area *clip, int x_mult, int y, uint8_t *dst, int dst_width, const FR_Bitmap *glyphs_bitmap, const FR_Bitmap_Glyph_Metrics *glyph, FR_Color color) { - agg::lcd_distribution_lut& lcd_lut = font_renderer->lcd_distribution_lut(); - const int subpixel_scale = font_renderer->subpixel_scale(); - const int pixel_size = 4; // Pixel size for BGRA32 format. - - int x = x_mult / subpixel_scale; - - x += glyph->xoff; - y += glyph->yoff; - - int glyph_x = glyph->x0, glyph_y = glyph->y0; - int glyph_x_subpixel = -(x_mult % subpixel_scale); - int glyph_width = glyph->x1 - glyph->x0; - int glyph_height = glyph->y1 - glyph->y0; - - int n; - if ((n = clip->left - x) > 0) { glyph_width -= n; glyph_x += n; x += n; } - if ((n = clip->top - y) > 0) { glyph_height -= n; glyph_y += n; y += n; } - if ((n = x + glyph_width - clip->right ) > 0) { glyph_width -= n; } - if ((n = y + glyph_height - clip->bottom) > 0) { glyph_height -= n; } - - if (glyph_width <= 0 || glyph_height <= 0) { - return; - } - - dst += (x + y * dst_width) * pixel_size; - agg::rendering_buffer dst_ren_buf(dst, glyph_width, glyph_height, dst_width * pixel_size); - - uint8_t *src = glyphs_bitmap->pixels + (glyph_x + glyph_y * glyphs_bitmap->width) * subpixel_scale + glyph_x_subpixel; - int src_stride = glyphs_bitmap->width * subpixel_scale; - - const agg::rgba8 color_a(color.r, color.g, color.b); - for (int x = 0, y = 0; y < glyph_height; y++) { - agg::int8u *covers = src + y * src_stride; - if (subpixel_scale == 1) { - blend_solid_hspan(dst_ren_buf, x, y, glyph_width, color_a, covers); - } else { - blend_solid_hspan_subpixel(dst_ren_buf, lcd_lut, x, y, glyph_width * subpixel_scale, color_a, covers); - } - } -} - diff --git a/lib/font_renderer/font_renderer.h b/lib/font_renderer/font_renderer.h deleted file mode 100644 index 019d2ae7..00000000 --- a/lib/font_renderer/font_renderer.h +++ /dev/null @@ -1,58 +0,0 @@ -#ifndef FONT_RENDERER_H -#define FONT_RENDERER_H - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -typedef struct { - unsigned short x0, y0, x1, y1; - float xoff, yoff, xadvance; -} FR_Bitmap_Glyph_Metrics; - -typedef struct FR_Bitmap FR_Bitmap; - -#ifdef __cplusplus -class FR_Renderer; -#else -struct FR_Renderer; -typedef struct FR_Renderer FR_Renderer; -#endif - -enum { - FR_HINTING = 1 << 0, - FR_KERNING = 1 << 1, - FR_SUBPIXEL = 1 << 2, - FR_PRESCALE_X = 1 << 3, -}; - -typedef struct { - uint8_t r, g, b; -} FR_Color; - -typedef struct { - int left, top, right, bottom; -} FR_Clip_Area; - -FR_Renderer * FR_Renderer_New(unsigned int flags); -void FR_Renderer_Free(FR_Renderer *); -int FR_Load_Font(FR_Renderer *, const char *filename); -FR_Bitmap* FR_Bitmap_New(FR_Renderer *, int width, int height); -void FR_Bitmap_Free(FR_Bitmap *image); -int FR_Get_Font_Height(FR_Renderer *, float size); -FR_Bitmap * FR_Bake_Font_Bitmap(FR_Renderer *, int font_height, - int first_char, int num_chars, FR_Bitmap_Glyph_Metrics *glyph_info); -void FR_Blend_Glyph(FR_Renderer *font_renderer, - FR_Clip_Area *clip, int x, int y, - uint8_t *dst, int dst_width, - const FR_Bitmap *glyphs_bitmap, - const FR_Bitmap_Glyph_Metrics *glyph, FR_Color color); -int FR_Subpixel_Scale(FR_Renderer *); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/lib/font_renderer/font_renderer_alpha.h b/lib/font_renderer/font_renderer_alpha.h deleted file mode 100644 index a0f45a27..00000000 --- a/lib/font_renderer/font_renderer_alpha.h +++ /dev/null @@ -1,164 +0,0 @@ -#pragma once - -#include "agg_basics.h" -#include "agg_conv_curve.h" -#include "agg_conv_transform.h" -#include "agg_gamma_lut.h" -#include "agg_font_freetype.h" -#include "agg_pixfmt_alpha8.h" -#include "agg_rasterizer_scanline_aa.h" -#include "agg_renderer_primitives.h" -#include "agg_renderer_scanline.h" -#include "agg_rendering_buffer.h" -#include "agg_scanline_u.h" - -class font_renderer_alpha -{ - typedef agg::pixfmt_alpha8 pixfmt_type; - typedef agg::renderer_base base_ren_type; - typedef agg::renderer_scanline_aa_solid renderer_solid; - typedef agg::font_engine_freetype_int32 font_engine_type; - typedef agg::font_cache_manager font_manager_type; - - font_engine_type m_feng; - font_manager_type m_fman; - - // Font rendering options. - bool m_hinting; - bool m_kerning; - bool m_subpixel; - bool m_prescale_x; - - bool m_font_loaded; - - // Pipeline to process the vectors glyph paths (curves + contour) - agg::trans_affine m_mtx; - agg::conv_curve m_curves; - agg::conv_transform > m_trans; -public: - typedef agg::pixfmt_alpha8::color_type color_type; - - font_renderer_alpha(bool hinting, bool kerning, bool subpixel, bool prescale_x): - m_feng(), - m_fman(m_feng), - m_hinting(hinting), - m_kerning(kerning), - m_subpixel(subpixel), - m_prescale_x(prescale_x), - m_font_loaded(false), - m_curves(m_fman.path_adaptor()), - m_trans(m_curves, m_mtx) - { } - - int get_face_height() const { - return m_feng.face_height(); - } - - void get_font_vmetrics(double& ascender, double& descender) { - double current_height = m_feng.height(); - m_feng.height(1.0); - ascender = m_feng.ascender(); - descender = m_feng.descender(); - m_feng.height(current_height); -} - - float scale_for_em_to_pixels(float size) { - int units_per_em = m_feng.face_units_em(); - if (units_per_em > 0) { - return size / units_per_em; - } - return 0.0; - } - - bool load_font(const char *font_filename) { - if(m_feng.load_font(font_filename, 0, agg::glyph_ren_outline)) { - m_font_loaded = true; - m_feng.hinting(m_hinting); - } - return m_font_loaded; - } - - void set_font_height(double height) { - const double scale_x = (m_prescale_x ? 100.0 : 1.0); - m_feng.height(height); - m_feng.width(height * scale_x); - } - - template - void draw_codepoint(Rasterizer& ras, Scanline& sl, - RenSolid& ren_solid, const color_type color, - int codepoint, double& x, double& y, const int subpixel_scale) - { - const double scale_x = (m_prescale_x ? 100.0 : 1.0); - // Coefficient to scale back the glyph to the final scale. - const double cx_inv_scale = subpixel_scale / scale_x; - - // Represent the delta in x scaled by scale_x. - double x_delta = 0; - double start_x = x; - - const agg::glyph_cache* glyph = m_fman.glyph(codepoint); - if(glyph) - { - if(m_kerning) - { - m_fman.add_kerning(&x_delta, &y); - } - - m_fman.init_embedded_adaptors(glyph, 0, 0); - if(glyph->data_type == agg::glyph_data_outline) - { - double ty = m_hinting ? floor(y + 0.5) : y; - ras.reset(); - m_mtx.reset(); - m_mtx *= agg::trans_affine_scaling(cx_inv_scale, 1); - m_mtx *= agg::trans_affine_translation(start_x + cx_inv_scale * x_delta, ty); - ras.add_path(m_trans); - ren_solid.color(color); - agg::render_scanlines(ras, sl, ren_solid); - } - - y += glyph->advance_y; - x += cx_inv_scale * (x_delta + glyph->advance_x); - } - } - - void clear(agg::rendering_buffer& ren_buf, const color_type color) { - pixfmt_type pf(ren_buf); - base_ren_type ren_base(pf); - ren_base.clear(color); - } - - void render_codepoint(agg::rendering_buffer& ren_buf, - const color_type text_color, - double& x, double& y, - int codepoint, const int subpixel_scale) - { - if (!m_font_loaded) { - return; - } - agg::scanline_u8 sl; - agg::rasterizer_scanline_aa<> ras; - ras.clip_box(0, 0, ren_buf.width(), ren_buf.height()); - - agg::pixfmt_alpha8 pf(ren_buf); - base_ren_type ren_base(pf); - renderer_solid ren_solid(ren_base); - draw_codepoint(ras, sl, ren_solid, text_color, codepoint, x, y, subpixel_scale); - } - - int codepoint_bounds(int codepoint, const int subpixel_scale, agg::rect_i& bounds) - { - if (!m_font_loaded) return 1; - const double scale_x = (m_prescale_x ? 100.0 : 1.0); - const double cx_inv_scale = subpixel_scale / scale_x; - const agg::glyph_cache* glyph = m_fman.glyph(codepoint); - if (glyph) { - bounds = glyph->bounds; - bounds.x1 *= cx_inv_scale; - bounds.x2 *= cx_inv_scale; - return 0; - } - return 1; - } -}; diff --git a/lib/font_renderer/meson.build b/lib/font_renderer/meson.build deleted file mode 100644 index 7724d584..00000000 --- a/lib/font_renderer/meson.build +++ /dev/null @@ -1,23 +0,0 @@ -freetype_dep = dependency('freetype2') - -libagg_dep = dependency('libagg', required: false) -if not libagg_dep.found() - libagg_subproject = subproject('libagg') - libagg_dep = libagg_subproject.get_variable('libagg_dep') -endif - -font_renderer_sources = [ - 'agg_font_freetype.cpp', - 'font_renderer.cpp', -] - -font_renderer_cdefs = ['-DFONT_RENDERER_HEIGHT_HACK'] - -font_renderer_include = include_directories('.') - -libfontrenderer = static_library('fontrenderer', - font_renderer_sources, - dependencies: [libagg_dep, freetype_dep], - cpp_args: font_renderer_cdefs, -) - diff --git a/lib/font_renderer/notes-lite-font-rendering.md b/lib/font_renderer/notes-lite-font-rendering.md deleted file mode 100644 index 175da8fc..00000000 --- a/lib/font_renderer/notes-lite-font-rendering.md +++ /dev/null @@ -1,125 +0,0 @@ -```c -stbtt_InitFont - -stbtt_ScaleForMappingEmToPixels x 3 -stbtt_ScaleForPixelHeight -stbtt_BakeFontBitmap -stbtt_GetFontVMetrics x 2 - -typedef struct { - unsigned short x0, y0, x1, y1; // coordinates of bbox in bitmap - float xoff, yoff, xadvance; -} stbtt_bakedchar; - -struct RenImage { - RenColor *pixels; - int width, height; -}; - -typedef struct { - RenImage *image; - stbtt_bakedchar glyphs[256]; -} GlyphSet; - -struct RenFont { - void *data; - stbtt_fontinfo stbfont; - GlyphSet *sets[MAX_GLYPHSET]; - float size; - int height; -}; - -``` - -The function stbtt_BakeFontBitmap is used to write bitmap data into set->image->pixels (where set is a GlyphSet). -Note that set->image->pixels need data in RGB format. After stbtt_BakeFontBitmap call the bitmap data are converted into RGB. -With a single call many glyphs corresponding to a range of codepoints, all in a -single image. - -## STB truetype font metrics - -stbtt_ScaleForPixelHeight takes a float 'height' and returns height / (ascent - descent). - -stbtt_ScaleForMappingEmToPixels take a float 'pixels' and returns pixels / unitsPerEm. - -### Computing RenFont - -When loading a font, in renderer.c, the font->height is determined as: - -```c -int ascent, descent, linegap; -stbtt_GetFontVMetrics(&font->stbfont, &ascent, &descent, &linegap); -float scale = stbtt_ScaleForMappingEmToPixels(&font->stbfont, font->size); -font->height = (ascent - descent + linegap) * scale + 0.5; -``` - -so, mathematically - -```c -font->height = (ascent - descent + linegap) * font->size / unitsPerEm + 0.5; -``` - -**TO DO**: find out for what font->height is actually used. - -### Call to BakeFontBitmap - -In the same file, renderer.c, to create the glyphset image it computes: - -```c -// Using stbtt functions -float s = ScaleForMappingEmToPixels(1) / ScaleForPixelHeight(1); -``` - -so 's' is actually equal to (ascent - descent) / unitsPerEm. - -Then BakeFontBitmap is called and `font->size * s` is used for the pixel_height argument. -So BakeFontBitmap gets - -```c -pixel_height = (ascent - descent) * font->size / unitsPerEm; -``` - -In turns BakeFontBitmap passes pixel_height to ScaleForPixelHeight() and uses the -resulting 'scale' to scale the glyphs by passing it to MakeGlyphBitmap(). - -This is equal almost equal to font->height except the 0.5, the missing linegap calculation -and the fact that this latter is an integer instead of a float. - -## AGG Font Engine - -Calls - -`FT_Init_FreeType` (initialize the library) - -In `load_font()` method: -`FT_New_Face` or `FT_New_Memory_Face` (use `FT_Done_Face` when done) - -`FT_Attach_File` -`FT_Select_Charmap` - -In `update_char_size()` method: -`FT_Set_Char_Size` or `FT_Set_Pixel_Sizes` - -In `prepare_glyph()` method: -`FT_Get_Char_Index` -`FT_Load_Glyph` -`FT_Render_Glyph` (if glyph_render_native_mono or native_gray8) - -in `add_kerning()` method -`FT_Get_Kerning` - -`FT_Done_FreeType` (end with library) - -## Freetype2's metrics related function and structs - -`FT_New_Face` return a `FT_Face` (a pointer) with font face information. - -## AGG font engine's font size - -The variable `m_height` is the size of the font muliplied by 64. -It will be used to set font size with: - -- `FT_Set_Char_Size` if m_resolution is set (> 0) -- `FT_Set_Pixel_Sizes`, divided by 64, if m_resolution is not set (= 0) - -The method height() returns m_height / 64; diff --git a/meson.build b/meson.build index 9240f68a..53883266 100644 --- a/meson.build +++ b/meson.build @@ -1,74 +1,127 @@ -project('lite-xl', 'c', 'cpp', default_options : ['c_std=gnu11', 'cpp_std=c++03']) +project('lite-xl', + ['c'], + version : '2.0.2', + license : 'MIT', + meson_version : '>= 0.54', + default_options : ['c_std=gnu11'] +) -version = get_option('version') +#=============================================================================== +# Configuration +#=============================================================================== conf_data = configuration_data() -conf_data.set('PROJECT_VERSION', version) +conf_data.set('PROJECT_BUILD_DIR', meson.current_build_dir()) +conf_data.set('PROJECT_SOURCE_DIR', meson.current_source_dir()) +conf_data.set('PROJECT_VERSION', meson.project_version()) +#=============================================================================== +# Compiler Settings +#=============================================================================== if host_machine.system() == 'darwin' add_languages('objc') endif cc = meson.get_compiler('c') -libm = cc.find_library('m', required : false) -libdl = cc.find_library('dl', required : false) -libx11 = dependency('x11', required : false) -lua_dep = dependency('lua5.2', required : false) -pcre2_dep = dependency('libpcre2-8') -sdl_dep = dependency('sdl2', method: 'config-tool') - -if not lua_dep.found() - lua_subproject = subproject('lua', default_options: ['shared=false', 'use_readline=false', 'app=false']) - lua_dep = lua_subproject.get_variable('lua_dep') -endif - -reproc_subproject = subproject('reproc', default_options: ['default_library=static', 'multithreaded=false', 'reproc-cpp=false', 'examples=false']) -reproc_dep = reproc_subproject.get_variable('reproc_dep') - -lite_deps = [lua_dep, sdl_dep, reproc_dep, pcre2_dep, libm, libdl, libx11] - -if host_machine.system() == 'windows' - # Note that we need to explicitly add the windows socket DLL because - # the pkg-config file from reproc does not include it. - lite_deps += meson.get_compiler('cpp').find_library('ws2_32', required: true) -endif lite_cargs = [] -if get_option('portable') - lite_docdir = 'doc' - lite_datadir = 'data' -else - lite_docdir = 'share/doc/lite-xl' - lite_datadir = 'share/lite-xl' -endif - -lite_include = include_directories('src') -foreach data_module : ['core', 'fonts', 'plugins', 'colors'] - install_subdir('data' / data_module , install_dir : lite_datadir) -endforeach - -install_data('licenses/licenses.md', install_dir : lite_docdir) - -lite_link_args = [] -if cc.get_id() == 'gcc' and get_option('buildtype') == 'release' - lite_link_args += ['-static-libgcc', '-static-libstdc++'] -endif -if host_machine.system() == 'darwin' - lite_link_args += ['-framework', 'CoreServices', '-framework', 'Foundation'] -endif - -lite_rc = [] -if host_machine.system() == 'windows' - windows = import('windows') - lite_rc += windows.compile_resources('resources/icons/icon.rc') - iss = configure_file(input : 'scripts/innosetup/innosetup.iss.in', - output : 'innosetup.iss', - configuration : conf_data) -endif - # On macos we need to use the SDL renderer to support retina displays if get_option('renderer') or host_machine.system() == 'darwin' lite_cargs += '-DLITE_USE_SDL_RENDERER' endif +#=============================================================================== +# Linker Settings +#=============================================================================== +lite_link_args = [] +if cc.get_id() == 'gcc' and get_option('buildtype') == 'release' + lite_link_args += ['-static-libgcc'] +endif -subdir('lib/font_renderer') -subdir('src') +if host_machine.system() == 'darwin' + lite_link_args += ['-framework', 'CoreServices', '-framework', 'Foundation'] +endif +#=============================================================================== +# Dependencies +#=============================================================================== +if not get_option('source-only') + libm = cc.find_library('m', required : false) + libdl = cc.find_library('dl', required : false) + lua_dep = dependency('lua5.2', fallback: ['lua', 'lua_dep'], + default_options: ['shared=false', 'use_readline=false', 'app=false'] + ) + pcre2_dep = dependency('libpcre2-8') + freetype_dep = dependency('freetype2') + sdl_dep = dependency('sdl2', method: 'config-tool') + reproc_dep = dependency('reproc', fallback: ['reproc', 'reproc_dep'], + default_options: [ + 'default_library=static', 'multithreaded=false', + 'reproc-cpp=false', 'examples=false' + ] + ) + + lite_deps = [lua_dep, sdl_dep, reproc_dep, pcre2_dep, libm, libdl, freetype_dep] + + if host_machine.system() == 'windows' + # Note that we need to explicitly add the windows socket DLL because + # the pkg-config file from reproc does not include it. + lite_deps += meson.get_compiler('c').find_library('ws2_32', required: true) + endif +endif +#=============================================================================== +# Install Configuration +#=============================================================================== +if get_option('portable') or host_machine.system() == 'windows' + lite_bindir = '/' + lite_docdir = '/doc' + lite_datadir = '/data' +elif get_option('bundle') and host_machine.system() == 'darwin' + lite_cargs += '-DMACOS_USE_BUNDLE' + lite_bindir = 'Contents/MacOS' + lite_docdir = 'Contents/Resources' + lite_datadir = 'Contents/Resources' + install_data('resources/icons/icon.icns', install_dir : 'Contents/Resources') + configure_file( + input : 'resources/macos/Info.plist.in', + output : 'Info.plist', + configuration : conf_data, + install : true, + install_dir : 'Contents' + ) +else + lite_bindir = 'bin' + lite_docdir = 'share/doc/lite-xl' + lite_datadir = 'share/lite-xl' + if host_machine.system() == 'linux' + install_data('resources/icons/lite-xl.svg', + install_dir : 'share/icons/hicolor/scalable/apps' + ) + install_data('resources/linux/org.lite_xl.lite_xl.desktop', + install_dir : 'share/applications' + ) + install_data('resources/linux/org.lite_xl.lite_xl.appdata.xml', + install_dir : 'share/metainfo' + ) + endif +endif + +install_data('licenses/licenses.md', install_dir : lite_docdir) + +install_subdir('data' / 'core' , install_dir : lite_datadir, exclude_files : 'start.lua') +foreach data_module : ['fonts', 'plugins', 'colors'] + install_subdir('data' / data_module , install_dir : lite_datadir) +endforeach + +configure_file( + input : 'data/core/start.lua', + output : 'start.lua', + configuration : conf_data, + install : true, + install_dir : lite_datadir / 'core', +) + +#=============================================================================== +# Targets +#=============================================================================== +if not get_option('source-only') + subdir('src') + subdir('scripts') +endif diff --git a/meson_options.txt b/meson_options.txt index a61bd359..1cf3e22f 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -1,4 +1,4 @@ -option('innosetup', type : 'boolean', value : false, description: 'Build Windows setup package') +option('bundle', type : 'boolean', value : false, description: 'Build a macOS bundle') +option('source-only', type : 'boolean', value : false, description: 'Configure source files only, doesn\'t checks for dependencies') option('portable', type : 'boolean', value : false, description: 'Portable install') option('renderer', type : 'boolean', value : false, description: 'Use SDL renderer') -option('version', type : 'string', value : '0.0.0', description: 'Project version') diff --git a/resources/icons/icon.icns b/resources/icons/icon.icns index 128045d8..71fea32f 100644 Binary files a/resources/icons/icon.icns and b/resources/icons/icon.icns differ diff --git a/resources/linux/org.lite_xl.lite_xl.appdata.xml b/resources/linux/org.lite_xl.lite_xl.appdata.xml new file mode 100644 index 00000000..c5895178 --- /dev/null +++ b/resources/linux/org.lite_xl.lite_xl.appdata.xml @@ -0,0 +1,33 @@ + + + org.lite_xl.lite_xl + MIT + MIT + Lite XL + A lightweight text editor written in Lua + + + +

+ Lite XL is a text editor and development tool written mainly in Lua, + on top of a minimalistic C core using the SDL2 graphics library. +

+
+ + + + The editor window + https://lite-xl.github.io/assets/img/screenshots/editor.png + + + + https://lite-xl.github.io + + + lite-xl + + + + + +
diff --git a/resources/linux/lite-xl.desktop b/resources/linux/org.lite_xl.lite_xl.desktop similarity index 71% rename from resources/linux/lite-xl.desktop rename to resources/linux/org.lite_xl.lite_xl.desktop index f2fa9610..d251c4dc 100644 --- a/resources/linux/lite-xl.desktop +++ b/resources/linux/org.lite_xl.lite_xl.desktop @@ -5,6 +5,6 @@ Comment=A lightweight text editor written in Lua Exec=lite-xl %F Icon=lite-xl Terminal=false -StartupNotify=false -Categories=Utility;TextEditor;Development; +StartupWMClass=lite-xl +Categories=Development;IDE; MimeType=text/plain; diff --git a/resources/lite_xl_plugin_api.h b/resources/lite_xl_plugin_api.h new file mode 100644 index 00000000..44c1b4d3 --- /dev/null +++ b/resources/lite_xl_plugin_api.h @@ -0,0 +1,360 @@ +#ifndef LITE_XL_PLUGIN_API +#define LITE_XL_PLUGIN_API +/* +The lite_xl plugin API is quite simple. Any shared library can be a plugin file, so long +as it has an entrypoint that looks like the following, where xxxxx is the plugin name: + +#include "lite_xl_plugin_api.h" + +int lua_open_lite_xl_xxxxx(lua_State* L, void* XL) { + lite_xl_plugin_init(XL); + ... + return 1; +} + +In linux, to compile this file, you'd do: 'gcc -o xxxxx.so -shared xxxxx.c'. Simple! +Due to the way the API is structured, you *should not* link or include lua libraries. + +This file was automatically generated by the below code. Do NOT MODIFY DIRECTLY. + + +#!/bin/sh +echo "#ifndef LITE_XL_PLUGIN_API" +echo "#define LITE_XL_PLUGIN_API" +echo "/* " +echo "The lite_xl plugin API is quite simple. Any shared library can be a plugin file, so long" +echo "as it has an entrypoint that looks like the following, where xxxxx is the plugin name:" +echo +echo '#include "lite_xl_plugin_api.h"' +echo +echo "int lua_open_lite_xl_xxxxx(lua_State* L, void* XL) {" +echo " lite_xl_plugin_init(XL);" +echo " ..." +echo " return 1;" +echo "}" +echo +echo "In linux, to compile this file, you'd do: 'gcc -o xxxxx.so -shared xxxxx.c'. Simple!" +echo "Due to the way the API is structured, you *should not* link or include lua libraries." +echo +echo "This file was automatically generated by the below code. Do NOT MODIFY DIRECTLY." +echo +echo +cat $0 +echo "*""/" +echo "#include " +echo "typedef struct lua_State lua_State; typedef double lua_Number; typedef int (*lua_CFunction)(lua_State*); typedef ptrdiff_t lua_Integer;" +echo "typedef unsigned long lua_Unsigned; typedef struct luaL_Buffer luaL_Buffer; typedef struct luaL_Reg luaL_Reg; typedef struct lua_Debug lua_Debug;" +echo "typedef void (*lua_Hook) (lua_State *L, lua_Debug *ar);" +echo "typedef void * (*lua_Alloc) (void *ud, void *ptr, size_t osize, size_t nsize);" +echo "typedef int (*lua_Writer) (lua_State *L, const void* p, size_t sz, void* ud);" +LUA_HEADERS=`pkg-config --cflags lua5.2 | sed 's/^-I//' | sed 's/$/\/*.h/'` +grep -h "^LUA\(LIB\)*_API" $LUA_HEADERS | sed "s/LUA\(LIB\)*_API //" | sed "s/(lua/(*lua/" | grep -v ",\s*$" | sed "s/^/static /" +grep -h "#define luaL*_" $LUA_HEADERS | grep -v "\\\s*$" | grep -v "\(assert\|lock\)" | grep -v "\(asm\|int32\)" | grep -v "#define lua_number2integer(i,n)\s*lua_number2int(i, n)" +echo "#define lua_pushliteral(L, s) lua_pushlstring(L, "" s, (sizeof(s)/sizeof(char))-1)" +echo "#define lua_pushglobaltable(L) lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS)" +echo "#define IMPORT_SYMBOL(name, ret, ...) name = (ret (*)(__VA_ARGS__))symbol(#name)" +echo "static void lite_xl_plugin_init(void* XL) {" +echo "\tvoid* (*symbol)(const char*) = (void* (*)(const char*))XL;" +grep -h "^LUA\(LIB\)*_API" $LUA_HEADERS | sed "s/LUA\(LIB\)*_API //" | sed "s/(lua/(*lua/" | grep -v ",\s*$" | sed "s/^\([^)]*\)(\*\(lua\w*\))\s*(/\tIMPORT_SYMBOL(\2, \1,/" +echo "}" +echo "#endif" +*/ +#include +typedef struct lua_State lua_State; typedef double lua_Number; typedef int (*lua_CFunction)(lua_State*); typedef ptrdiff_t lua_Integer; +typedef unsigned long lua_Unsigned; typedef struct luaL_Buffer luaL_Buffer; typedef struct luaL_Reg luaL_Reg; typedef struct lua_Debug lua_Debug; +typedef void (*lua_Hook) (lua_State *L, lua_Debug *ar); +typedef void * (*lua_Alloc) (void *ud, void *ptr, size_t osize, size_t nsize); +typedef int (*lua_Writer) (lua_State *L, const void* p, size_t sz, void* ud); +static void (*luaL_checkversion_) (lua_State *L, lua_Number ver); +static int (*luaL_getmetafield) (lua_State *L, int obj, const char *e); +static int (*luaL_callmeta) (lua_State *L, int obj, const char *e); +static const char *(*luaL_tolstring) (lua_State *L, int idx, size_t *len); +static int (*luaL_argerror) (lua_State *L, int numarg, const char *extramsg); +static lua_Number (*luaL_checknumber) (lua_State *L, int numArg); +static lua_Number (*luaL_optnumber) (lua_State *L, int nArg, lua_Number def); +static lua_Integer (*luaL_checkinteger) (lua_State *L, int numArg); +static lua_Unsigned (*luaL_checkunsigned) (lua_State *L, int numArg); +static void (*luaL_checkstack) (lua_State *L, int sz, const char *msg); +static void (*luaL_checktype) (lua_State *L, int narg, int t); +static void (*luaL_checkany) (lua_State *L, int narg); +static int (*luaL_newmetatable) (lua_State *L, const char *tname); +static void (*luaL_setmetatable) (lua_State *L, const char *tname); +static void *(*luaL_testudata) (lua_State *L, int ud, const char *tname); +static void *(*luaL_checkudata) (lua_State *L, int ud, const char *tname); +static void (*luaL_where) (lua_State *L, int lvl); +static int (*luaL_error) (lua_State *L, const char *fmt, ...); +static int (*luaL_fileresult) (lua_State *L, int stat, const char *fname); +static int (*luaL_execresult) (lua_State *L, int stat); +static int (*luaL_ref) (lua_State *L, int t); +static void (*luaL_unref) (lua_State *L, int t, int ref); +static int (*luaL_loadstring) (lua_State *L, const char *s); +static lua_State *(*luaL_newstate) (void); +static int (*luaL_len) (lua_State *L, int idx); +static void (*luaL_setfuncs) (lua_State *L, const luaL_Reg *l, int nup); +static int (*luaL_getsubtable) (lua_State *L, int idx, const char *fname); +static void (*luaL_buffinit) (lua_State *L, luaL_Buffer *B); +static char *(*luaL_prepbuffsize) (luaL_Buffer *B, size_t sz); +static void (*luaL_addlstring) (luaL_Buffer *B, const char *s, size_t l); +static void (*luaL_addstring) (luaL_Buffer *B, const char *s); +static void (*luaL_addvalue) (luaL_Buffer *B); +static void (*luaL_pushresult) (luaL_Buffer *B); +static void (*luaL_pushresultsize) (luaL_Buffer *B, size_t sz); +static char *(*luaL_buffinitsize) (lua_State *L, luaL_Buffer *B, size_t sz); +static lua_State *(*lua_newstate) (lua_Alloc f, void *ud); +static void (*lua_close) (lua_State *L); +static lua_State *(*lua_newthread) (lua_State *L); +static lua_CFunction (*lua_atpanic) (lua_State *L, lua_CFunction panicf); +static const lua_Number *(*lua_version) (lua_State *L); +static int (*lua_absindex) (lua_State *L, int idx); +static int (*lua_gettop) (lua_State *L); +static void (*lua_settop) (lua_State *L, int idx); +static void (*lua_pushvalue) (lua_State *L, int idx); +static void (*lua_remove) (lua_State *L, int idx); +static void (*lua_insert) (lua_State *L, int idx); +static void (*lua_replace) (lua_State *L, int idx); +static void (*lua_copy) (lua_State *L, int fromidx, int toidx); +static int (*lua_checkstack) (lua_State *L, int sz); +static void (*lua_xmove) (lua_State *from, lua_State *to, int n); +static int (*lua_isnumber) (lua_State *L, int idx); +static int (*lua_isstring) (lua_State *L, int idx); +static int (*lua_iscfunction) (lua_State *L, int idx); +static int (*lua_isuserdata) (lua_State *L, int idx); +static int (*lua_type) (lua_State *L, int idx); +static const char *(*lua_typename) (lua_State *L, int tp); +static lua_Number (*lua_tonumberx) (lua_State *L, int idx, int *isnum); +static lua_Integer (*lua_tointegerx) (lua_State *L, int idx, int *isnum); +static lua_Unsigned (*lua_tounsignedx) (lua_State *L, int idx, int *isnum); +static int (*lua_toboolean) (lua_State *L, int idx); +static const char *(*lua_tolstring) (lua_State *L, int idx, size_t *len); +static size_t (*lua_rawlen) (lua_State *L, int idx); +static lua_CFunction (*lua_tocfunction) (lua_State *L, int idx); +static void *(*lua_touserdata) (lua_State *L, int idx); +static lua_State *(*lua_tothread) (lua_State *L, int idx); +static const void *(*lua_topointer) (lua_State *L, int idx); +static void (*lua_arith) (lua_State *L, int op); +static int (*lua_rawequal) (lua_State *L, int idx1, int idx2); +static int (*lua_compare) (lua_State *L, int idx1, int idx2, int op); +static void (*lua_pushnil) (lua_State *L); +static void (*lua_pushnumber) (lua_State *L, lua_Number n); +static void (*lua_pushinteger) (lua_State *L, lua_Integer n); +static void (*lua_pushunsigned) (lua_State *L, lua_Unsigned n); +static const char *(*lua_pushlstring) (lua_State *L, const char *s, size_t l); +static const char *(*lua_pushstring) (lua_State *L, const char *s); +static const char *(*lua_pushfstring) (lua_State *L, const char *fmt, ...); +static void (*lua_pushcclosure) (lua_State *L, lua_CFunction fn, int n); +static void (*lua_pushboolean) (lua_State *L, int b); +static void (*lua_pushlightuserdata) (lua_State *L, void *p); +static int (*lua_pushthread) (lua_State *L); +static void (*lua_getglobal) (lua_State *L, const char *var); +static void (*lua_gettable) (lua_State *L, int idx); +static void (*lua_getfield) (lua_State *L, int idx, const char *k); +static void (*lua_rawget) (lua_State *L, int idx); +static void (*lua_rawgeti) (lua_State *L, int idx, int n); +static void (*lua_rawgetp) (lua_State *L, int idx, const void *p); +static void (*lua_createtable) (lua_State *L, int narr, int nrec); +static void *(*lua_newuserdata) (lua_State *L, size_t sz); +static int (*lua_getmetatable) (lua_State *L, int objindex); +static void (*lua_getuservalue) (lua_State *L, int idx); +static void (*lua_setglobal) (lua_State *L, const char *var); +static void (*lua_settable) (lua_State *L, int idx); +static void (*lua_setfield) (lua_State *L, int idx, const char *k); +static void (*lua_rawset) (lua_State *L, int idx); +static void (*lua_rawseti) (lua_State *L, int idx, int n); +static void (*lua_rawsetp) (lua_State *L, int idx, const void *p); +static int (*lua_setmetatable) (lua_State *L, int objindex); +static void (*lua_setuservalue) (lua_State *L, int idx); +static int (*lua_getctx) (lua_State *L, int *ctx); +static int (*lua_dump) (lua_State *L, lua_Writer writer, void *data); +static int (*lua_resume) (lua_State *L, lua_State *from, int narg); +static int (*lua_status) (lua_State *L); +static int (*lua_gc) (lua_State *L, int what, int data); +static int (*lua_error) (lua_State *L); +static int (*lua_next) (lua_State *L, int idx); +static void (*lua_concat) (lua_State *L, int n); +static void (*lua_len) (lua_State *L, int idx); +static lua_Alloc (*lua_getallocf) (lua_State *L, void **ud); +static void (*lua_setallocf) (lua_State *L, lua_Alloc f, void *ud); +static int (*lua_getstack) (lua_State *L, int level, lua_Debug *ar); +static int (*lua_getinfo) (lua_State *L, const char *what, lua_Debug *ar); +static const char *(*lua_getlocal) (lua_State *L, const lua_Debug *ar, int n); +static const char *(*lua_setlocal) (lua_State *L, const lua_Debug *ar, int n); +static const char *(*lua_getupvalue) (lua_State *L, int funcindex, int n); +static const char *(*lua_setupvalue) (lua_State *L, int funcindex, int n); +static void *(*lua_upvalueid) (lua_State *L, int fidx, int n); +static int (*lua_sethook) (lua_State *L, lua_Hook func, int mask, int count); +static lua_Hook (*lua_gethook) (lua_State *L); +static int (*lua_gethookmask) (lua_State *L); +static int (*lua_gethookcount) (lua_State *L); +static void (*luaL_openlibs) (lua_State *L); +#define luaL_checkversion(L) luaL_checkversion_(L, LUA_VERSION_NUM) +#define luaL_loadfile(L,f) luaL_loadfilex(L,f,NULL) +#define luaL_newlib(L,l) (luaL_newlibtable(L,l), luaL_setfuncs(L,l,0)) +#define luaL_checkstring(L,n) (luaL_checklstring(L, (n), NULL)) +#define luaL_optstring(L,n,d) (luaL_optlstring(L, (n), (d), NULL)) +#define luaL_checkint(L,n) ((int)luaL_checkinteger(L, (n))) +#define luaL_optint(L,n,d) ((int)luaL_optinteger(L, (n), (d))) +#define luaL_checklong(L,n) ((long)luaL_checkinteger(L, (n))) +#define luaL_optlong(L,n,d) ((long)luaL_optinteger(L, (n), (d))) +#define luaL_typename(L,i) lua_typename(L, lua_type(L,(i))) +#define luaL_getmetatable(L,n) (lua_getfield(L, LUA_REGISTRYINDEX, (n))) +#define luaL_opt(L,f,n,d) (lua_isnoneornil(L,(n)) ? (d) : f(L,(n))) +#define luaL_loadbuffer(L,s,sz,n) luaL_loadbufferx(L,s,sz,n,NULL) +#define luaL_addsize(B,s) ((B)->n += (s)) +#define luaL_prepbuffer(B) luaL_prepbuffsize(B, LUAL_BUFFERSIZE) +#define luaL_register(L,n,l) (luaL_openlib(L,(n),(l),0)) +#define lua_h +#define lua_upvalueindex(i) (LUA_REGISTRYINDEX - (i)) +#define lua_call(L,n,r) lua_callk(L, (n), (r), 0, NULL) +#define lua_pcall(L,n,r,f) lua_pcallk(L, (n), (r), (f), 0, NULL) +#define lua_yield(L,n) lua_yieldk(L, (n), 0, NULL) +#define lua_tonumber(L,i) lua_tonumberx(L,i,NULL) +#define lua_tointeger(L,i) lua_tointegerx(L,i,NULL) +#define lua_tounsigned(L,i) lua_tounsignedx(L,i,NULL) +#define lua_pop(L,n) lua_settop(L, -(n)-1) +#define lua_newtable(L) lua_createtable(L, 0, 0) +#define lua_register(L,n,f) (lua_pushcfunction(L, (f)), lua_setglobal(L, (n))) +#define lua_pushcfunction(L,f) lua_pushcclosure(L, (f), 0) +#define lua_isfunction(L,n) (lua_type(L, (n)) == LUA_TFUNCTION) +#define lua_istable(L,n) (lua_type(L, (n)) == LUA_TTABLE) +#define lua_islightuserdata(L,n) (lua_type(L, (n)) == LUA_TLIGHTUSERDATA) +#define lua_isnil(L,n) (lua_type(L, (n)) == LUA_TNIL) +#define lua_isboolean(L,n) (lua_type(L, (n)) == LUA_TBOOLEAN) +#define lua_isthread(L,n) (lua_type(L, (n)) == LUA_TTHREAD) +#define lua_isnone(L,n) (lua_type(L, (n)) == LUA_TNONE) +#define lua_isnoneornil(L, n) (lua_type(L, (n)) <= 0) +#define lua_tostring(L,i) lua_tolstring(L, (i), NULL) +#define lua_strlen(L,i) lua_rawlen(L, (i)) +#define lua_objlen(L,i) lua_rawlen(L, (i)) +#define lua_equal(L,idx1,idx2) lua_compare(L,(idx1),(idx2),LUA_OPEQ) +#define lua_lessthan(L,idx1,idx2) lua_compare(L,(idx1),(idx2),LUA_OPLT) +#define lua_number2str(s,n) sprintf((s), LUA_NUMBER_FMT, (n)) +#define lua_str2number(s,p) strtod((s), (p)) +#define lua_strx2number(s,p) strtod((s), (p)) +#define lua_pushliteral(L, s) lua_pushlstring(L, s, (sizeof(s)/sizeof(char))-1) +#define lua_pushglobaltable(L) lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_RIDX_GLOBALS) +#define IMPORT_SYMBOL(name, ret, ...) name = (ret (*)(__VA_ARGS__))symbol(#name) +static void lite_xl_plugin_init(void* XL) { + void* (*symbol)(const char*) = (void* (*)(const char*))XL; + IMPORT_SYMBOL(luaL_checkversion_, void ,lua_State *L, lua_Number ver); + IMPORT_SYMBOL(luaL_getmetafield, int ,lua_State *L, int obj, const char *e); + IMPORT_SYMBOL(luaL_callmeta, int ,lua_State *L, int obj, const char *e); + IMPORT_SYMBOL(luaL_tolstring, const char *,lua_State *L, int idx, size_t *len); + IMPORT_SYMBOL(luaL_argerror, int ,lua_State *L, int numarg, const char *extramsg); + IMPORT_SYMBOL(luaL_checknumber, lua_Number ,lua_State *L, int numArg); + IMPORT_SYMBOL(luaL_optnumber, lua_Number ,lua_State *L, int nArg, lua_Number def); + IMPORT_SYMBOL(luaL_checkinteger, lua_Integer ,lua_State *L, int numArg); + IMPORT_SYMBOL(luaL_checkunsigned, lua_Unsigned ,lua_State *L, int numArg); + IMPORT_SYMBOL(luaL_checkstack, void ,lua_State *L, int sz, const char *msg); + IMPORT_SYMBOL(luaL_checktype, void ,lua_State *L, int narg, int t); + IMPORT_SYMBOL(luaL_checkany, void ,lua_State *L, int narg); + IMPORT_SYMBOL(luaL_newmetatable, int ,lua_State *L, const char *tname); + IMPORT_SYMBOL(luaL_setmetatable, void ,lua_State *L, const char *tname); + IMPORT_SYMBOL(luaL_testudata, void *,lua_State *L, int ud, const char *tname); + IMPORT_SYMBOL(luaL_checkudata, void *,lua_State *L, int ud, const char *tname); + IMPORT_SYMBOL(luaL_where, void ,lua_State *L, int lvl); + IMPORT_SYMBOL(luaL_error, int ,lua_State *L, const char *fmt, ...); + IMPORT_SYMBOL(luaL_fileresult, int ,lua_State *L, int stat, const char *fname); + IMPORT_SYMBOL(luaL_execresult, int ,lua_State *L, int stat); + IMPORT_SYMBOL(luaL_ref, int ,lua_State *L, int t); + IMPORT_SYMBOL(luaL_unref, void ,lua_State *L, int t, int ref); + IMPORT_SYMBOL(luaL_loadstring, int ,lua_State *L, const char *s); + IMPORT_SYMBOL(luaL_newstate, lua_State *,void); + IMPORT_SYMBOL(luaL_len, int ,lua_State *L, int idx); + IMPORT_SYMBOL(luaL_setfuncs, void ,lua_State *L, const luaL_Reg *l, int nup); + IMPORT_SYMBOL(luaL_getsubtable, int ,lua_State *L, int idx, const char *fname); + IMPORT_SYMBOL(luaL_buffinit, void ,lua_State *L, luaL_Buffer *B); + IMPORT_SYMBOL(luaL_prepbuffsize, char *,luaL_Buffer *B, size_t sz); + IMPORT_SYMBOL(luaL_addlstring, void ,luaL_Buffer *B, const char *s, size_t l); + IMPORT_SYMBOL(luaL_addstring, void ,luaL_Buffer *B, const char *s); + IMPORT_SYMBOL(luaL_addvalue, void ,luaL_Buffer *B); + IMPORT_SYMBOL(luaL_pushresult, void ,luaL_Buffer *B); + IMPORT_SYMBOL(luaL_pushresultsize, void ,luaL_Buffer *B, size_t sz); + IMPORT_SYMBOL(luaL_buffinitsize, char *,lua_State *L, luaL_Buffer *B, size_t sz); + IMPORT_SYMBOL(lua_newstate, lua_State *,lua_Alloc f, void *ud); + IMPORT_SYMBOL(lua_close, void ,lua_State *L); + IMPORT_SYMBOL(lua_newthread, lua_State *,lua_State *L); + IMPORT_SYMBOL(lua_atpanic, lua_CFunction ,lua_State *L, lua_CFunction panicf); + IMPORT_SYMBOL(lua_version, const lua_Number *,lua_State *L); + IMPORT_SYMBOL(lua_absindex, int ,lua_State *L, int idx); + IMPORT_SYMBOL(lua_gettop, int ,lua_State *L); + IMPORT_SYMBOL(lua_settop, void ,lua_State *L, int idx); + IMPORT_SYMBOL(lua_pushvalue, void ,lua_State *L, int idx); + IMPORT_SYMBOL(lua_remove, void ,lua_State *L, int idx); + IMPORT_SYMBOL(lua_insert, void ,lua_State *L, int idx); + IMPORT_SYMBOL(lua_replace, void ,lua_State *L, int idx); + IMPORT_SYMBOL(lua_copy, void ,lua_State *L, int fromidx, int toidx); + IMPORT_SYMBOL(lua_checkstack, int ,lua_State *L, int sz); + IMPORT_SYMBOL(lua_xmove, void ,lua_State *from, lua_State *to, int n); + IMPORT_SYMBOL(lua_isnumber, int ,lua_State *L, int idx); + IMPORT_SYMBOL(lua_isstring, int ,lua_State *L, int idx); + IMPORT_SYMBOL(lua_iscfunction, int ,lua_State *L, int idx); + IMPORT_SYMBOL(lua_isuserdata, int ,lua_State *L, int idx); + IMPORT_SYMBOL(lua_type, int ,lua_State *L, int idx); + IMPORT_SYMBOL(lua_typename, const char *,lua_State *L, int tp); + IMPORT_SYMBOL(lua_tonumberx, lua_Number ,lua_State *L, int idx, int *isnum); + IMPORT_SYMBOL(lua_tointegerx, lua_Integer ,lua_State *L, int idx, int *isnum); + IMPORT_SYMBOL(lua_tounsignedx, lua_Unsigned ,lua_State *L, int idx, int *isnum); + IMPORT_SYMBOL(lua_toboolean, int ,lua_State *L, int idx); + IMPORT_SYMBOL(lua_tolstring, const char *,lua_State *L, int idx, size_t *len); + IMPORT_SYMBOL(lua_rawlen, size_t ,lua_State *L, int idx); + IMPORT_SYMBOL(lua_tocfunction, lua_CFunction ,lua_State *L, int idx); + IMPORT_SYMBOL(lua_touserdata, void *,lua_State *L, int idx); + IMPORT_SYMBOL(lua_tothread, lua_State *,lua_State *L, int idx); + IMPORT_SYMBOL(lua_topointer, const void *,lua_State *L, int idx); + IMPORT_SYMBOL(lua_arith, void ,lua_State *L, int op); + IMPORT_SYMBOL(lua_rawequal, int ,lua_State *L, int idx1, int idx2); + IMPORT_SYMBOL(lua_compare, int ,lua_State *L, int idx1, int idx2, int op); + IMPORT_SYMBOL(lua_pushnil, void ,lua_State *L); + IMPORT_SYMBOL(lua_pushnumber, void ,lua_State *L, lua_Number n); + IMPORT_SYMBOL(lua_pushinteger, void ,lua_State *L, lua_Integer n); + IMPORT_SYMBOL(lua_pushunsigned, void ,lua_State *L, lua_Unsigned n); + IMPORT_SYMBOL(lua_pushlstring, const char *,lua_State *L, const char *s, size_t l); + IMPORT_SYMBOL(lua_pushstring, const char *,lua_State *L, const char *s); + IMPORT_SYMBOL(lua_pushfstring, const char *,lua_State *L, const char *fmt, ...); + IMPORT_SYMBOL(lua_pushcclosure, void ,lua_State *L, lua_CFunction fn, int n); + IMPORT_SYMBOL(lua_pushboolean, void ,lua_State *L, int b); + IMPORT_SYMBOL(lua_pushlightuserdata, void ,lua_State *L, void *p); + IMPORT_SYMBOL(lua_pushthread, int ,lua_State *L); + IMPORT_SYMBOL(lua_getglobal, void ,lua_State *L, const char *var); + IMPORT_SYMBOL(lua_gettable, void ,lua_State *L, int idx); + IMPORT_SYMBOL(lua_getfield, void ,lua_State *L, int idx, const char *k); + IMPORT_SYMBOL(lua_rawget, void ,lua_State *L, int idx); + IMPORT_SYMBOL(lua_rawgeti, void ,lua_State *L, int idx, int n); + IMPORT_SYMBOL(lua_rawgetp, void ,lua_State *L, int idx, const void *p); + IMPORT_SYMBOL(lua_createtable, void ,lua_State *L, int narr, int nrec); + IMPORT_SYMBOL(lua_newuserdata, void *,lua_State *L, size_t sz); + IMPORT_SYMBOL(lua_getmetatable, int ,lua_State *L, int objindex); + IMPORT_SYMBOL(lua_getuservalue, void ,lua_State *L, int idx); + IMPORT_SYMBOL(lua_setglobal, void ,lua_State *L, const char *var); + IMPORT_SYMBOL(lua_settable, void ,lua_State *L, int idx); + IMPORT_SYMBOL(lua_setfield, void ,lua_State *L, int idx, const char *k); + IMPORT_SYMBOL(lua_rawset, void ,lua_State *L, int idx); + IMPORT_SYMBOL(lua_rawseti, void ,lua_State *L, int idx, int n); + IMPORT_SYMBOL(lua_rawsetp, void ,lua_State *L, int idx, const void *p); + IMPORT_SYMBOL(lua_setmetatable, int ,lua_State *L, int objindex); + IMPORT_SYMBOL(lua_setuservalue, void ,lua_State *L, int idx); + IMPORT_SYMBOL(lua_getctx, int ,lua_State *L, int *ctx); + IMPORT_SYMBOL(lua_dump, int ,lua_State *L, lua_Writer writer, void *data); + IMPORT_SYMBOL(lua_resume, int ,lua_State *L, lua_State *from, int narg); + IMPORT_SYMBOL(lua_status, int ,lua_State *L); + IMPORT_SYMBOL(lua_gc, int ,lua_State *L, int what, int data); + IMPORT_SYMBOL(lua_error, int ,lua_State *L); + IMPORT_SYMBOL(lua_next, int ,lua_State *L, int idx); + IMPORT_SYMBOL(lua_concat, void ,lua_State *L, int n); + IMPORT_SYMBOL(lua_len, void ,lua_State *L, int idx); + IMPORT_SYMBOL(lua_getallocf, lua_Alloc ,lua_State *L, void **ud); + IMPORT_SYMBOL(lua_setallocf, void ,lua_State *L, lua_Alloc f, void *ud); + IMPORT_SYMBOL(lua_getstack, int ,lua_State *L, int level, lua_Debug *ar); + IMPORT_SYMBOL(lua_getinfo, int ,lua_State *L, const char *what, lua_Debug *ar); + IMPORT_SYMBOL(lua_getlocal, const char *,lua_State *L, const lua_Debug *ar, int n); + IMPORT_SYMBOL(lua_setlocal, const char *,lua_State *L, const lua_Debug *ar, int n); + IMPORT_SYMBOL(lua_getupvalue, const char *,lua_State *L, int funcindex, int n); + IMPORT_SYMBOL(lua_setupvalue, const char *,lua_State *L, int funcindex, int n); + IMPORT_SYMBOL(lua_upvalueid, void *,lua_State *L, int fidx, int n); + IMPORT_SYMBOL(lua_sethook, int ,lua_State *L, lua_Hook func, int mask, int count); + IMPORT_SYMBOL(lua_gethook, lua_Hook ,lua_State *L); + IMPORT_SYMBOL(lua_gethookmask, int ,lua_State *L); + IMPORT_SYMBOL(lua_gethookcount, int ,lua_State *L); + IMPORT_SYMBOL(luaL_openlibs, void ,lua_State *L); +} +#endif diff --git a/resources/macos/Info.plist b/resources/macos/Info.plist.in similarity index 53% rename from resources/macos/Info.plist rename to resources/macos/Info.plist.in index cc369cd0..4d715f2f 100644 --- a/resources/macos/Info.plist +++ b/resources/macos/Info.plist.in @@ -2,25 +2,30 @@ -CFBundleExecutable + CFBundleExecutable lite-xl CFBundleGetInfoString lite-xl CFBundleIconFile - icon + icon.icns CFBundleName - lite-xl + Lite XL CFBundlePackageType APPL NSHighResolutionCapable - MinimumOSVersion10.13 - NSDocumentsFolderUsageDescriptionTo access, edit and index your projects. - NSDesktopFolderUsageDescriptionTo access, edit and index your projects. - NSDownloadsFolderUsageDescriptionTo access, edit and index your projects. + LSMinimumSystemVersion + 10.11 + NSDocumentsFolderUsageDescription + To access, edit and index your projects. + NSDesktopFolderUsageDescription + To access, edit and index your projects. + NSDownloadsFolderUsageDescription + To access, edit and index your projects. CFBundleShortVersionString - 1.16.10 + @PROJECT_VERSION@ NSHumanReadableCopyright © 2019-2021 Francesco Abbate + diff --git a/resources/macos/appdmg.png b/resources/macos/appdmg.png new file mode 100644 index 00000000..1df7b60d Binary files /dev/null and b/resources/macos/appdmg.png differ diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 00000000..7d66fbca --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,29 @@ +# Scripts + +Various scripts and configurations used to configure, build, and package Lite XL. + +### Build + +- **build.sh** +- **build-packages.sh**: In root directory, as all in one script; relies to the + ones in this directory. + +### Package + +- **appdmg.sh**: Create a macOS DMG image using [AppDMG][1]. +- **appimage.sh**: [AppImage][2] builder. +- **innosetup.sh**: Creates a 32/64 bit [InnoSetup][3] installer package. +- **package.sh**: Creates all binary / DMG image / installer / source packages. + +### Utility + +- **common.sh**: Common functions used by other scripts. +- **install-dependencies.sh**: Installs required applications to build, package + and run Lite XL, mainly useful for CI and documentation purpose. + Preferably not to be used in user systems. +- **fontello-config.json**: Used by the icons generator. +- **keymap-generator**: Generates a JSON file containing the keymap + +[1]: https://github.com/LinusU/node-appdmg +[2]: https://docs.appimage.org/ +[3]: https://jrsoftware.org/isinfo.php diff --git a/scripts/appdmg.sh b/scripts/appdmg.sh new file mode 100644 index 00000000..840f518b --- /dev/null +++ b/scripts/appdmg.sh @@ -0,0 +1,30 @@ +#!/bin/bash +set -ex + +if [ ! -e "src/api/api.h" ]; then + echo "Please run this script from the root directory of Lite XL." + exit 1 +fi + +cat > lite-xl-dmg.json << EOF +{ + "title": "Lite XL", + "icon": "$(pwd)/resources/icons/icon.icns", + "background": "$(pwd)/resources/macos/appdmg.png", + "window": { + "position": { + "x": 360, + "y": 360 + }, + "size": { + "width": 480, + "height": 360 + } + }, + "contents": [ + { "x": 144, "y": 248, "type": "file", "path": "$(pwd)/Lite XL.app" }, + { "x": 336, "y": 248, "type": "link", "path": "/Applications" } + ] +} +EOF +~/node_modules/appdmg/bin/appdmg.js lite-xl-dmg.json "$(pwd)/$1.dmg" diff --git a/scripts/appimage.sh b/scripts/appimage.sh new file mode 100644 index 00000000..8844fafe --- /dev/null +++ b/scripts/appimage.sh @@ -0,0 +1,162 @@ +#!/bin/env bash +set -ex + +if [ ! -e "src/api/api.h" ]; then + echo "Please run this script from the root directory of Lite XL." + exit 1 +fi + +source scripts/common.sh + +show_help(){ + echo + echo "Usage: $0 " + echo + echo "Available options:" + echo + echo "-h --help Show this help and exits." + echo "-b --builddir DIRNAME Sets the name of the build dir (no path)." + echo " Default: 'build'." + echo "-n --nobuild Skips the build step, use existing files." + echo "-s --static Specify if building using static libraries" + echo " by using lhelper tool." + echo "-v --version VERSION Specify a version, non whitespace separated string." + echo +} + +ARCH="$(uname -m)" +BUILD_DIR="$(get_default_build_dir)" +RUN_BUILD=true +STATIC_BUILD=false + +for i in "$@"; do + case $i in + -h|--belp) + show_help + exit 0 + ;; + -b|--builddir) + BUILD_DIR="$2" + shift + shift + ;; + -n|--nobuild) + RUN_BUILD=false + shift + ;; + -s|--static) + STATIC_BUILD=true + shift + ;; + -v|--version) + VERSION="$2" + shift + shift + ;; + *) + # unknown option + ;; + esac +done + +# TODO: Versioning using git +#if [[ -z $VERSION && -d .git ]]; then +# VERSION=$(git describe --tags --long | sed 's/^v//; s/\([^-]*-g\)/r\1/; s/-/./g') +#fi + +if [[ -n $1 ]]; then + show_help + exit 1 +fi + +setup_appimagetool() { + if ! which appimagetool > /dev/null ; then + if [ ! -e appimagetool ]; then + if ! wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-${ARCH}.AppImage" ; then + echo "Could not download the appimagetool for the arch '${ARCH}'." + exit 1 + else + chmod 0755 appimagetool + fi + fi + fi +} + +download_appimage_apprun() { + if [ ! -e AppRun ]; then + if ! wget -O AppRun "https://github.com/AppImage/AppImageKit/releases/download/continuous/AppRun-${ARCH}" ; then + echo "Could not download AppRun for the arch '${ARCH}'." + exit 1 + else + chmod 0755 AppRun + fi + fi +} + +build_litexl() { + if [ -e build ]; then + rm -rf build + fi + + if [ -e ${BUILD_DIR} ]; then + rm -rf ${BUILD_DIR} + fi + + echo "Build lite-xl..." + sleep 1 + meson setup --buildtype=release --prefix /usr ${BUILD_DIR} + meson compile -C ${BUILD_DIR} +} + +generate_appimage() { + if [ -e LiteXL.AppDir ]; then + rm -rf LiteXL.AppDir + fi + + echo "Creating LiteXL.AppDir..." + + DESTDIR="$(realpath LiteXL.AppDir)" meson install --skip-subprojects -C ${BUILD_DIR} + mv AppRun LiteXL.AppDir/ + # These could be symlinks but it seems they doesn't work with AppimageLauncher + cp resources/icons/lite-xl.svg LiteXL.AppDir/ + cp resources/linux/org.lite_xl.lite_xl.desktop LiteXL.AppDir/ + + if [[ $STATIC_BUILD == false ]]; then + echo "Copying libraries..." + + mkdir -p LiteXL.AppDir/usr/lib/ + + local allowed_libs=( + libfreetype + libpcre2 + libSDL2 + libsndio + liblua + ) + + while read line; do + local libname="$(echo $line | cut -d' ' -f1)" + local libpath="$(echo $line | cut -d' ' -f2)" + for lib in "${allowed_libs[@]}" ; do + if echo "$libname" | grep "$lib" > /dev/null ; then + cp "$libpath" LiteXL.AppDir/usr/lib/ + continue 2 + fi + done + echo " Ignoring: $libname" + done < <(ldd build/src/lite-xl | awk '{print $1 " " $3}') + fi + + echo "Generating AppImage..." + local version="" + if [ -n "$VERSION" ]; then + version="-$VERSION" + fi + + ./appimagetool LiteXL.AppDir LiteXL${version}-${ARCH}.AppImage +} + +setup_appimagetool +download_appimage_apprun +if [[ $RUN_BUILD == true ]]; then build_litexl; fi +generate_appimage $1 diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 00000000..75212468 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,117 @@ +#!/bin/bash +set -e + +if [ ! -e "src/api/api.h" ]; then + echo "Please run this script from the root directory of Lite XL."; exit 1 +fi + +source scripts/common.sh + +show_help() { + echo + echo "Usage: $0 " + echo + echo "Available options:" + echo + echo "-b --builddir DIRNAME Sets the name of the build directory (not path)." + echo " Default: '$(get_default_build_dir)'." + echo " --debug Debug this script." + echo "-f --forcefallback Force to build dependencies statically." + echo "-h --help Show this help and exit." + echo "-p --prefix PREFIX Install directory prefix. Default: '/'." + echo "-B --bundle Create an App bundle (macOS only)" + echo "-P --portable Create a portable binary package." + echo "-O --pgo Use profile guided optimizations (pgo)." + echo " macOS: disabled when used with --bundle," + echo " Windows: Implicit being the only option." + echo +} + +main() { + local platform="$(get_platform_name)" + local build_dir="$(get_default_build_dir)" + local prefix=/ + local force_fallback + local bundle + local portable + local pgo + + for i in "$@"; do + case $i in + -h|--help) + show_help + exit 0 + ;; + -b|--builddir) + build_dir="$2" + shift + shift + ;; + --debug) + set -x + shift + ;; + -f|--forcefallback) + force_fallback="--wrap-mode=forcefallback" + shift + ;; + -p|--prefix) + prefix="$2" + shift + shift + ;; + -B|--bundle) + if [[ "$platform" != "macos" ]]; then + echo "Warning: ignoring --bundle option, works only under macOS." + else + bundle="-Dbundle=true" + fi + shift + ;; + -P|--portable) + portable="-Dportable=true" + shift + ;; + -O|--pgo) + pgo="-Db_pgo=generate" + shift + ;; + *) + # unknown option + ;; + esac + done + + if [[ -n $1 ]]; then + show_help + exit 1 + fi + + if [[ $platform == "macos" && -n $bundle && -n $portable ]]; then + echo "Warning: \"bundle\" and \"portable\" specified; excluding portable package." + portable="" + fi + + rm -rf "${build_dir}" + + CFLAGS=$CFLAGS LDFLAGS=$LDFLAGS meson setup \ + --buildtype=release \ + --prefix "$prefix" \ + $force_fallback \ + $bundle \ + $portable \ + $pgo \ + "${build_dir}" + + meson compile -C "${build_dir}" + + if [ ! -z ${pgo+x} ]; then + cp -r data "${build_dir}/src" + "${build_dir}/src/lite-xl" + meson configure -Db_pgo=use "${build_dir}" + meson compile -C "${build_dir}" + rm -fr "${build_dir}/data" + fi +} + +main "$@" diff --git a/scripts/common.sh b/scripts/common.sh new file mode 100644 index 00000000..2b49d362 --- /dev/null +++ b/scripts/common.sh @@ -0,0 +1,25 @@ +#!/bin/bash + +set -e + +get_platform_name() { + if [[ "$OSTYPE" == "msys" ]]; then + echo "windows" + elif [[ "$OSTYPE" == "darwin"* ]]; then + echo "macos" + elif [[ "$OSTYPE" == "linux"* || "$OSTYPE" == "freebsd"* ]]; then + echo "linux" + else + echo "UNSUPPORTED-OS" + fi +} + +get_default_build_dir() { + platform=$(get_platform_name) + echo "build-$platform-$(uname -m)" +} + +if [[ $(get_platform_name) == "UNSUPPORTED-OS" ]]; then + echo "Error: unknown OS type: \"$OSTYPE\"" + exit 1 +fi diff --git a/scripts/innosetup/innosetup.iss.in b/scripts/innosetup/innosetup.iss.in index f085d271..2b669fc0 100644 --- a/scripts/innosetup/innosetup.iss.in +++ b/scripts/innosetup/innosetup.iss.in @@ -4,12 +4,12 @@ #define MyAppURL "https://lite-xl.github.io" #define MyAppExeName "lite-xl.exe" #define BuildDir "@PROJECT_BUILD_DIR@" -#define SourceDir "." +#define SourceDir "@PROJECT_SOURCE_DIR@" ; Use /dArch option to create a setup for a different architecture, e.g.: ; iscc /dArch=x86 innosetup.iss #ifndef Arch -#define Arch "x64" + #define Arch "x64" #endif [Setup] @@ -27,14 +27,17 @@ AppSupportURL={#MyAppURL} AppUpdatesURL={#MyAppURL} #if Arch=="x64" -ArchitecturesAllowed=x64 -ArchitecturesInstallIn64BitMode={#Arch} + ArchitecturesAllowed=x64 + ArchitecturesInstallIn64BitMode=x64 + #define ArchInternal "x86_64" +#else + #define ArchInternal "i686" #endif AllowNoIcons=yes Compression=lzma SolidCompression=yes -DefaultDirName={autopf}\{#MyAppName} +DefaultDirName={autopf}/{#MyAppName} DefaultGroupName={#MyAppPublisher} UninstallFilesDir={app} @@ -48,14 +51,14 @@ PrivilegesRequiredOverridesAllowed=dialog UsedUserAreasWarning=no OutputDir=. -OutputBaseFilename=LiteXL-{#MyAppVersion}-{#Arch}-setup +OutputBaseFilename=LiteXL-{#MyAppVersion}-{#ArchInternal}-setup ;DisableDirPage=yes ;DisableProgramGroupPage=yes -LicenseFile={#SourceDir}\LICENSE -SetupIconFile={#SourceDir}\icon.ico -WizardImageFile="wizard-modern-image.bmp" -WizardSmallImageFile="litexl-55px.bmp" +LicenseFile={#SourceDir}/LICENSE +SetupIconFile={#SourceDir}/resources/icons/icon.ico +WizardImageFile="{#SourceDir}/scripts/innosetup/wizard-modern-image.bmp" +WizardSmallImageFile="{#SourceDir}/scripts/innosetup/litexl-55px.bmp" [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" @@ -66,18 +69,20 @@ Name: "quicklaunchicon"; Description: "{cm:CreateQuickLaunchIcon}"; GroupDescrip Name: "portablemode"; Description: "Portable Mode"; Flags: unchecked [Files] -Source: "{#BuildDir}\lite-xl.exe"; DestDir: "{app}"; Flags: ignoreversion -Source: "{#BuildDir}\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs +Source: "{#BuildDir}/src/lite-xl.exe"; DestDir: "{app}"; Flags: ignoreversion +Source: "{#BuildDir}/mingwLibs{#Arch}/*"; DestDir: "{app}"; Flags: ignoreversion ; Check: DirExists(ExpandConstant('{#BuildDir}/mingwLibs{#Arch}')) +Source: "{#SourceDir}/data/*"; DestDir: "{app}/data"; Flags: ignoreversion recursesubdirs ; NOTE: Don't use "Flags: ignoreversion" on any shared system files [Icons] -Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: not IsTaskSelected('portablemode') -Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"; Check: not IsTaskSelected('portablemode') Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon -Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon; Check: not IsTaskSelected('portablemode') +Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Check: not WizardIsTaskSelected('portablemode') +Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"; Check: not WizardIsTaskSelected('portablemode') +Name: "{userappdata}\Microsoft\Internet Explorer\Quick Launch\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: quicklaunchicon; Check: not WizardIsTaskSelected('portablemode') +; Name: "{usersendto}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" [Run] -Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent +Filename: "{app}/{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent [Setup] -Uninstallable=not IsTaskSelected('portablemode') +Uninstallable=not WizardIsTaskSelected('portablemode') diff --git a/scripts/innosetup/innosetup.sh b/scripts/innosetup/innosetup.sh new file mode 100644 index 00000000..4384d13c --- /dev/null +++ b/scripts/innosetup/innosetup.sh @@ -0,0 +1,65 @@ +#!/bin/bash +set -e + +if [ ! -e "src/api/api.h" ]; then + echo "Please run this script from the root directory of Lite XL."; exit 1 +fi + +source scripts/common.sh + +show_help() { + echo + echo "Usage: $0 " + echo + echo "Available options:" + echo + echo "-b --builddir DIRNAME Sets the name of the build directory (not path)." + echo " Default: '$(get_default_build_dir)'." + echo " --debug Debug this script." + echo +} + +main() { + local build_dir=$(get_default_build_dir) + local arch + + if [[ $MSYSTEM == "MINGW64" ]]; then arch=x64; else arch=Win32; fi + + for i in "$@"; do + case $i in + -h|--help) + show_help + exit 0 + ;; + -b|--builddir) + build_dir="$2" + shift + shift + ;; + --debug) + set -x + shift + ;; + *) + # unknown option + ;; + esac + done + + if [[ -n $1 ]]; then + show_help + exit 1 + fi + + # Copy MinGW libraries dependencies. + # MSYS2 ldd command seems to be only 64bit, so use ntldd + # see https://github.com/msys2/MINGW-packages/issues/4164 + local mingwLibsDir="${build_dir}/mingwLibs$arch" + mkdir -p "$mingwLibsDir" + ntldd -R "${build_dir}/src/lite-xl.exe" | grep mingw | awk '{print $3}' | sed 's#\\#/#g' | xargs -I '{}' cp -v '{}' $mingwLibsDir + + "/c/Program Files (x86)/Inno Setup 6/ISCC.exe" -dARCH=$arch "${build_dir}/scripts/innosetup.iss" + pushd "${build_dir}/scripts"; mv LiteXL*.exe "./../../"; popd +} + +main "$@" diff --git a/scripts/install-dependencies.sh b/scripts/install-dependencies.sh new file mode 100644 index 00000000..2f9519b1 --- /dev/null +++ b/scripts/install-dependencies.sh @@ -0,0 +1,74 @@ +#!/bin/bash +set -ex + +if [ ! -e "src/api/api.h" ]; then + echo "Please run this script from the root directory of Lite XL."; exit 1 +fi + +show_help() { + echo + echo "Lite XL dependecies installer. Mainly used for CI but can also work on users systems." + echo "USE IT AT YOUR OWN RISK!" + echo + echo "Usage: $0 " + echo + echo "Available options:" + echo + echo "-l --lhelper Install tools required by LHelper and doesn't" + echo " install external libraries." + echo " --debug Debug this script." + echo +} + +main() { + local lhelper=false + + for i in "$@"; do + case $i in + -s|--lhelper) + lhelper=true + shift + ;; + --debug) + set -x + shift + ;; + *) + # unknown option + ;; + esac + done + + if [[ -n $1 ]]; then + show_help + exit 1 + fi + + if [[ "$OSTYPE" == "linux"* ]]; then + if [[ $lhelper == true ]]; then + sudo apt-get install -qq ninja-build + else + sudo apt-get install -qq ninja-build libsdl2-dev libfreetype6 + fi + pip3 install meson + elif [[ "$OSTYPE" == "darwin"* ]]; then + if [[ $lhelper == true ]]; then + brew install bash md5sha1sum ninja + else + brew install bash ninja sdl2 + fi + pip3 install meson + cd ~; npm install appdmg; cd - + ~/node_modules/appdmg/bin/appdmg.js --version + elif [[ "$OSTYPE" == "msys" ]]; then + if [[ $lhelper == true ]]; then + pacman --noconfirm -S \ + ${MINGW_PACKAGE_PREFIX}-{gcc,meson,ninja,ntldd,pkg-config} unzip + else + pacman --noconfirm -S \ + ${MINGW_PACKAGE_PREFIX}-{gcc,meson,ninja,ntldd,pkg-config,freetype,pcre2,SDL2} unzip + fi + fi +} + +main "$@" diff --git a/scripts/keymap-generator/dkjson.lua b/scripts/keymap-generator/dkjson.lua new file mode 100644 index 00000000..fa50b9fa --- /dev/null +++ b/scripts/keymap-generator/dkjson.lua @@ -0,0 +1,714 @@ +-- Module options: +local always_try_using_lpeg = true +local register_global_module_table = false +local global_module_name = 'json' + +--[==[ + +David Kolf's JSON module for Lua 5.1/5.2 + +Version 2.5 + + +For the documentation see the corresponding readme.txt or visit +. + +You can contact the author by sending an e-mail to 'david' at the +domain 'dkolf.de'. + + +Copyright (C) 2010-2013 David Heiko Kolf + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--]==] + +-- global dependencies: +local pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset = + pairs, type, tostring, tonumber, getmetatable, setmetatable, rawset +local error, require, pcall, select = error, require, pcall, select +local floor, huge = math.floor, math.huge +local strrep, gsub, strsub, strbyte, strchar, strfind, strlen, strformat = + string.rep, string.gsub, string.sub, string.byte, string.char, + string.find, string.len, string.format +local strmatch = string.match +local concat = table.concat + +local json = { version = "dkjson 2.5" } + +if register_global_module_table then + _G[global_module_name] = json +end + +local _ENV = nil -- blocking globals in Lua 5.2 + +pcall (function() + -- Enable access to blocked metatables. + -- Don't worry, this module doesn't change anything in them. + local debmeta = require "debug".getmetatable + if debmeta then getmetatable = debmeta end +end) + +json.null = setmetatable ({}, { + __tojson = function () return "null" end +}) + +local function isarray (tbl) + local max, n, arraylen = 0, 0, 0 + for k,v in pairs (tbl) do + if k == 'n' and type(v) == 'number' then + arraylen = v + if v > max then + max = v + end + else + if type(k) ~= 'number' or k < 1 or floor(k) ~= k then + return false + end + if k > max then + max = k + end + n = n + 1 + end + end + if max > 10 and max > arraylen and max > n * 2 then + return false -- don't create an array with too many holes + end + return true, max +end + +local escapecodes = { + ["\""] = "\\\"", ["\\"] = "\\\\", ["\b"] = "\\b", ["\f"] = "\\f", + ["\n"] = "\\n", ["\r"] = "\\r", ["\t"] = "\\t" +} + +local function escapeutf8 (uchar) + local value = escapecodes[uchar] + if value then + return value + end + local a, b, c, d = strbyte (uchar, 1, 4) + a, b, c, d = a or 0, b or 0, c or 0, d or 0 + if a <= 0x7f then + value = a + elseif 0xc0 <= a and a <= 0xdf and b >= 0x80 then + value = (a - 0xc0) * 0x40 + b - 0x80 + elseif 0xe0 <= a and a <= 0xef and b >= 0x80 and c >= 0x80 then + value = ((a - 0xe0) * 0x40 + b - 0x80) * 0x40 + c - 0x80 + elseif 0xf0 <= a and a <= 0xf7 and b >= 0x80 and c >= 0x80 and d >= 0x80 then + value = (((a - 0xf0) * 0x40 + b - 0x80) * 0x40 + c - 0x80) * 0x40 + d - 0x80 + else + return "" + end + if value <= 0xffff then + return strformat ("\\u%.4x", value) + elseif value <= 0x10ffff then + -- encode as UTF-16 surrogate pair + value = value - 0x10000 + local highsur, lowsur = 0xD800 + floor (value/0x400), 0xDC00 + (value % 0x400) + return strformat ("\\u%.4x\\u%.4x", highsur, lowsur) + else + return "" + end +end + +local function fsub (str, pattern, repl) + -- gsub always builds a new string in a buffer, even when no match + -- exists. First using find should be more efficient when most strings + -- don't contain the pattern. + if strfind (str, pattern) then + return gsub (str, pattern, repl) + else + return str + end +end + +local function quotestring (value) + -- based on the regexp "escapable" in https://github.com/douglascrockford/JSON-js + value = fsub (value, "[%z\1-\31\"\\\127]", escapeutf8) + if strfind (value, "[\194\216\220\225\226\239]") then + value = fsub (value, "\194[\128-\159\173]", escapeutf8) + value = fsub (value, "\216[\128-\132]", escapeutf8) + value = fsub (value, "\220\143", escapeutf8) + value = fsub (value, "\225\158[\180\181]", escapeutf8) + value = fsub (value, "\226\128[\140-\143\168-\175]", escapeutf8) + value = fsub (value, "\226\129[\160-\175]", escapeutf8) + value = fsub (value, "\239\187\191", escapeutf8) + value = fsub (value, "\239\191[\176-\191]", escapeutf8) + end + return "\"" .. value .. "\"" +end +json.quotestring = quotestring + +local function replace(str, o, n) + local i, j = strfind (str, o, 1, true) + if i then + return strsub(str, 1, i-1) .. n .. strsub(str, j+1, -1) + else + return str + end +end + +-- locale independent num2str and str2num functions +local decpoint, numfilter + +local function updatedecpoint () + decpoint = strmatch(tostring(0.5), "([^05+])") + -- build a filter that can be used to remove group separators + numfilter = "[^0-9%-%+eE" .. gsub(decpoint, "[%^%$%(%)%%%.%[%]%*%+%-%?]", "%%%0") .. "]+" +end + +updatedecpoint() + +local function num2str (num) + return replace(fsub(tostring(num), numfilter, ""), decpoint, ".") +end + +local function str2num (str) + local num = tonumber(replace(str, ".", decpoint)) + if not num then + updatedecpoint() + num = tonumber(replace(str, ".", decpoint)) + end + return num +end + +local function addnewline2 (level, buffer, buflen) + buffer[buflen+1] = "\n" + buffer[buflen+2] = strrep (" ", level) + buflen = buflen + 2 + return buflen +end + +function json.addnewline (state) + if state.indent then + state.bufferlen = addnewline2 (state.level or 0, + state.buffer, state.bufferlen or #(state.buffer)) + end +end + +local encode2 -- forward declaration + +local function addpair (key, value, prev, indent, level, buffer, buflen, tables, globalorder, state) + local kt = type (key) + if kt ~= 'string' and kt ~= 'number' then + return nil, "type '" .. kt .. "' is not supported as a key by JSON." + end + if prev then + buflen = buflen + 1 + buffer[buflen] = "," + end + if indent then + buflen = addnewline2 (level, buffer, buflen) + end + buffer[buflen+1] = quotestring (key) + buffer[buflen+2] = ":" + return encode2 (value, indent, level, buffer, buflen + 2, tables, globalorder, state) +end + +local function appendcustom(res, buffer, state) + local buflen = state.bufferlen + if type (res) == 'string' then + buflen = buflen + 1 + buffer[buflen] = res + end + return buflen +end + +local function exception(reason, value, state, buffer, buflen, defaultmessage) + defaultmessage = defaultmessage or reason + local handler = state.exception + if not handler then + return nil, defaultmessage + else + state.bufferlen = buflen + local ret, msg = handler (reason, value, state, defaultmessage) + if not ret then return nil, msg or defaultmessage end + return appendcustom(ret, buffer, state) + end +end + +function json.encodeexception(reason, value, state, defaultmessage) + return quotestring("<" .. defaultmessage .. ">") +end + +encode2 = function (value, indent, level, buffer, buflen, tables, globalorder, state) + local valtype = type (value) + local valmeta = getmetatable (value) + valmeta = type (valmeta) == 'table' and valmeta -- only tables + local valtojson = valmeta and valmeta.__tojson + if valtojson then + if tables[value] then + return exception('reference cycle', value, state, buffer, buflen) + end + tables[value] = true + state.bufferlen = buflen + local ret, msg = valtojson (value, state) + if not ret then return exception('custom encoder failed', value, state, buffer, buflen, msg) end + tables[value] = nil + buflen = appendcustom(ret, buffer, state) + elseif value == nil then + buflen = buflen + 1 + buffer[buflen] = "null" + elseif valtype == 'number' then + local s + if value ~= value or value >= huge or -value >= huge then + -- This is the behaviour of the original JSON implementation. + s = "null" + else + s = num2str (value) + end + buflen = buflen + 1 + buffer[buflen] = s + elseif valtype == 'boolean' then + buflen = buflen + 1 + buffer[buflen] = value and "true" or "false" + elseif valtype == 'string' then + buflen = buflen + 1 + buffer[buflen] = quotestring (value) + elseif valtype == 'table' then + if tables[value] then + return exception('reference cycle', value, state, buffer, buflen) + end + tables[value] = true + level = level + 1 + local isa, n = isarray (value) + if n == 0 and valmeta and valmeta.__jsontype == 'object' then + isa = false + end + local msg + if isa then -- JSON array + buflen = buflen + 1 + buffer[buflen] = "[" + for i = 1, n do + buflen, msg = encode2 (value[i], indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + if i < n then + buflen = buflen + 1 + buffer[buflen] = "," + end + end + buflen = buflen + 1 + buffer[buflen] = "]" + else -- JSON object + local prev = false + buflen = buflen + 1 + buffer[buflen] = "{" + local order = valmeta and valmeta.__jsonorder or globalorder + if order then + local used = {} + n = #order + for i = 1, n do + local k = order[i] + local v = value[k] + if v then + used[k] = true + buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + prev = true -- add a seperator before the next element + end + end + for k,v in pairs (value) do + if not used[k] then + buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + prev = true -- add a seperator before the next element + end + end + else -- unordered + for k,v in pairs (value) do + buflen, msg = addpair (k, v, prev, indent, level, buffer, buflen, tables, globalorder, state) + if not buflen then return nil, msg end + prev = true -- add a seperator before the next element + end + end + if indent then + buflen = addnewline2 (level - 1, buffer, buflen) + end + buflen = buflen + 1 + buffer[buflen] = "}" + end + tables[value] = nil + else + return exception ('unsupported type', value, state, buffer, buflen, + "type '" .. valtype .. "' is not supported by JSON.") + end + return buflen +end + +function json.encode (value, state) + state = state or {} + local oldbuffer = state.buffer + local buffer = oldbuffer or {} + state.buffer = buffer + updatedecpoint() + local ret, msg = encode2 (value, state.indent, state.level or 0, + buffer, state.bufferlen or 0, state.tables or {}, state.keyorder, state) + if not ret then + error (msg, 2) + elseif oldbuffer == buffer then + state.bufferlen = ret + return true + else + state.bufferlen = nil + state.buffer = nil + return concat (buffer) + end +end + +local function loc (str, where) + local line, pos, linepos = 1, 1, 0 + while true do + pos = strfind (str, "\n", pos, true) + if pos and pos < where then + line = line + 1 + linepos = pos + pos = pos + 1 + else + break + end + end + return "line " .. line .. ", column " .. (where - linepos) +end + +local function unterminated (str, what, where) + return nil, strlen (str) + 1, "unterminated " .. what .. " at " .. loc (str, where) +end + +local function scanwhite (str, pos) + while true do + pos = strfind (str, "%S", pos) + if not pos then return nil end + local sub2 = strsub (str, pos, pos + 1) + if sub2 == "\239\187" and strsub (str, pos + 2, pos + 2) == "\191" then + -- UTF-8 Byte Order Mark + pos = pos + 3 + elseif sub2 == "//" then + pos = strfind (str, "[\n\r]", pos + 2) + if not pos then return nil end + elseif sub2 == "/*" then + pos = strfind (str, "*/", pos + 2) + if not pos then return nil end + pos = pos + 2 + else + return pos + end + end +end + +local escapechars = { + ["\""] = "\"", ["\\"] = "\\", ["/"] = "/", ["b"] = "\b", ["f"] = "\f", + ["n"] = "\n", ["r"] = "\r", ["t"] = "\t" +} + +local function unichar (value) + if value < 0 then + return nil + elseif value <= 0x007f then + return strchar (value) + elseif value <= 0x07ff then + return strchar (0xc0 + floor(value/0x40), + 0x80 + (floor(value) % 0x40)) + elseif value <= 0xffff then + return strchar (0xe0 + floor(value/0x1000), + 0x80 + (floor(value/0x40) % 0x40), + 0x80 + (floor(value) % 0x40)) + elseif value <= 0x10ffff then + return strchar (0xf0 + floor(value/0x40000), + 0x80 + (floor(value/0x1000) % 0x40), + 0x80 + (floor(value/0x40) % 0x40), + 0x80 + (floor(value) % 0x40)) + else + return nil + end +end + +local function scanstring (str, pos) + local lastpos = pos + 1 + local buffer, n = {}, 0 + while true do + local nextpos = strfind (str, "[\"\\]", lastpos) + if not nextpos then + return unterminated (str, "string", pos) + end + if nextpos > lastpos then + n = n + 1 + buffer[n] = strsub (str, lastpos, nextpos - 1) + end + if strsub (str, nextpos, nextpos) == "\"" then + lastpos = nextpos + 1 + break + else + local escchar = strsub (str, nextpos + 1, nextpos + 1) + local value + if escchar == "u" then + value = tonumber (strsub (str, nextpos + 2, nextpos + 5), 16) + if value then + local value2 + if 0xD800 <= value and value <= 0xDBff then + -- we have the high surrogate of UTF-16. Check if there is a + -- low surrogate escaped nearby to combine them. + if strsub (str, nextpos + 6, nextpos + 7) == "\\u" then + value2 = tonumber (strsub (str, nextpos + 8, nextpos + 11), 16) + if value2 and 0xDC00 <= value2 and value2 <= 0xDFFF then + value = (value - 0xD800) * 0x400 + (value2 - 0xDC00) + 0x10000 + else + value2 = nil -- in case it was out of range for a low surrogate + end + end + end + value = value and unichar (value) + if value then + if value2 then + lastpos = nextpos + 12 + else + lastpos = nextpos + 6 + end + end + end + end + if not value then + value = escapechars[escchar] or escchar + lastpos = nextpos + 2 + end + n = n + 1 + buffer[n] = value + end + end + if n == 1 then + return buffer[1], lastpos + elseif n > 1 then + return concat (buffer), lastpos + else + return "", lastpos + end +end + +local scanvalue -- forward declaration + +local function scantable (what, closechar, str, startpos, nullval, objectmeta, arraymeta) + local len = strlen (str) + local tbl, n = {}, 0 + local pos = startpos + 1 + if what == 'object' then + setmetatable (tbl, objectmeta) + else + setmetatable (tbl, arraymeta) + end + while true do + pos = scanwhite (str, pos) + if not pos then return unterminated (str, what, startpos) end + local char = strsub (str, pos, pos) + if char == closechar then + return tbl, pos + 1 + end + local val1, err + val1, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta) + if err then return nil, pos, err end + pos = scanwhite (str, pos) + if not pos then return unterminated (str, what, startpos) end + char = strsub (str, pos, pos) + if char == ":" then + if val1 == nil then + return nil, pos, "cannot use nil as table index (at " .. loc (str, pos) .. ")" + end + pos = scanwhite (str, pos + 1) + if not pos then return unterminated (str, what, startpos) end + local val2 + val2, pos, err = scanvalue (str, pos, nullval, objectmeta, arraymeta) + if err then return nil, pos, err end + tbl[val1] = val2 + pos = scanwhite (str, pos) + if not pos then return unterminated (str, what, startpos) end + char = strsub (str, pos, pos) + else + n = n + 1 + tbl[n] = val1 + end + if char == "," then + pos = pos + 1 + end + end +end + +scanvalue = function (str, pos, nullval, objectmeta, arraymeta) + pos = pos or 1 + pos = scanwhite (str, pos) + if not pos then + return nil, strlen (str) + 1, "no valid JSON value (reached the end)" + end + local char = strsub (str, pos, pos) + if char == "{" then + return scantable ('object', "}", str, pos, nullval, objectmeta, arraymeta) + elseif char == "[" then + return scantable ('array', "]", str, pos, nullval, objectmeta, arraymeta) + elseif char == "\"" then + return scanstring (str, pos) + else + local pstart, pend = strfind (str, "^%-?[%d%.]+[eE]?[%+%-]?%d*", pos) + if pstart then + local number = str2num (strsub (str, pstart, pend)) + if number then + return number, pend + 1 + end + end + pstart, pend = strfind (str, "^%a%w*", pos) + if pstart then + local name = strsub (str, pstart, pend) + if name == "true" then + return true, pend + 1 + elseif name == "false" then + return false, pend + 1 + elseif name == "null" then + return nullval, pend + 1 + end + end + return nil, pos, "no valid JSON value at " .. loc (str, pos) + end +end + +local function optionalmetatables(...) + if select("#", ...) > 0 then + return ... + else + return {__jsontype = 'object'}, {__jsontype = 'array'} + end +end + +function json.decode (str, pos, nullval, ...) + local objectmeta, arraymeta = optionalmetatables(...) + return scanvalue (str, pos, nullval, objectmeta, arraymeta) +end + +function json.use_lpeg () + local g = require ("lpeg") + + if g.version() == "0.11" then + error "due to a bug in LPeg 0.11, it cannot be used for JSON matching" + end + + local pegmatch = g.match + local P, S, R = g.P, g.S, g.R + + local function ErrorCall (str, pos, msg, state) + if not state.msg then + state.msg = msg .. " at " .. loc (str, pos) + state.pos = pos + end + return false + end + + local function Err (msg) + return g.Cmt (g.Cc (msg) * g.Carg (2), ErrorCall) + end + + local SingleLineComment = P"//" * (1 - S"\n\r")^0 + local MultiLineComment = P"/*" * (1 - P"*/")^0 * P"*/" + local Space = (S" \n\r\t" + P"\239\187\191" + SingleLineComment + MultiLineComment)^0 + + local PlainChar = 1 - S"\"\\\n\r" + local EscapeSequence = (P"\\" * g.C (S"\"\\/bfnrt" + Err "unsupported escape sequence")) / escapechars + local HexDigit = R("09", "af", "AF") + local function UTF16Surrogate (match, pos, high, low) + high, low = tonumber (high, 16), tonumber (low, 16) + if 0xD800 <= high and high <= 0xDBff and 0xDC00 <= low and low <= 0xDFFF then + return true, unichar ((high - 0xD800) * 0x400 + (low - 0xDC00) + 0x10000) + else + return false + end + end + local function UTF16BMP (hex) + return unichar (tonumber (hex, 16)) + end + local U16Sequence = (P"\\u" * g.C (HexDigit * HexDigit * HexDigit * HexDigit)) + local UnicodeEscape = g.Cmt (U16Sequence * U16Sequence, UTF16Surrogate) + U16Sequence/UTF16BMP + local Char = UnicodeEscape + EscapeSequence + PlainChar + local String = P"\"" * g.Cs (Char ^ 0) * (P"\"" + Err "unterminated string") + local Integer = P"-"^(-1) * (P"0" + (R"19" * R"09"^0)) + local Fractal = P"." * R"09"^0 + local Exponent = (S"eE") * (S"+-")^(-1) * R"09"^1 + local Number = (Integer * Fractal^(-1) * Exponent^(-1))/str2num + local Constant = P"true" * g.Cc (true) + P"false" * g.Cc (false) + P"null" * g.Carg (1) + local SimpleValue = Number + String + Constant + local ArrayContent, ObjectContent + + -- The functions parsearray and parseobject parse only a single value/pair + -- at a time and store them directly to avoid hitting the LPeg limits. + local function parsearray (str, pos, nullval, state) + local obj, cont + local npos + local t, nt = {}, 0 + repeat + obj, cont, npos = pegmatch (ArrayContent, str, pos, nullval, state) + if not npos then break end + pos = npos + nt = nt + 1 + t[nt] = obj + until cont == 'last' + return pos, setmetatable (t, state.arraymeta) + end + + local function parseobject (str, pos, nullval, state) + local obj, key, cont + local npos + local t = {} + repeat + key, obj, cont, npos = pegmatch (ObjectContent, str, pos, nullval, state) + if not npos then break end + pos = npos + t[key] = obj + until cont == 'last' + return pos, setmetatable (t, state.objectmeta) + end + + local Array = P"[" * g.Cmt (g.Carg(1) * g.Carg(2), parsearray) * Space * (P"]" + Err "']' expected") + local Object = P"{" * g.Cmt (g.Carg(1) * g.Carg(2), parseobject) * Space * (P"}" + Err "'}' expected") + local Value = Space * (Array + Object + SimpleValue) + local ExpectedValue = Value + Space * Err "value expected" + ArrayContent = Value * Space * (P"," * g.Cc'cont' + g.Cc'last') * g.Cp() + local Pair = g.Cg (Space * String * Space * (P":" + Err "colon expected") * ExpectedValue) + ObjectContent = Pair * Space * (P"," * g.Cc'cont' + g.Cc'last') * g.Cp() + local DecodeValue = ExpectedValue * g.Cp () + + function json.decode (str, pos, nullval, ...) + local state = {} + state.objectmeta, state.arraymeta = optionalmetatables(...) + local obj, retpos = pegmatch (DecodeValue, str, pos, nullval, state) + if state.msg then + return nil, state.pos, state.msg + else + return obj, retpos + end + end + + -- use this function only once: + json.use_lpeg = function () return json end + + json.using_lpeg = true + + return json -- so you can get the module using json = require "dkjson".use_lpeg() +end + +if always_try_using_lpeg then + pcall (json.use_lpeg) +end + +return json + diff --git a/scripts/keymap-generator/keymap-generator.lua b/scripts/keymap-generator/keymap-generator.lua new file mode 100644 index 00000000..235d48c2 --- /dev/null +++ b/scripts/keymap-generator/keymap-generator.lua @@ -0,0 +1,75 @@ +#!/usr/bin/env lua +local dkjson = require "dkjson" + + +local function load_keymap(target, target_map, macos) + _G.MACOS = macos + package.loaded["core.keymap"] = nil + local keymap = require "core.keymap" + + if target then + keymap.map = {} + require(target) + end + + target_map = target_map or {} + -- keymap.reverse_map does not do this? + for key, actions in pairs(keymap.map) do + for _, action in ipairs(actions) do + target_map[action] = target_map[action] or {} + table.insert(target_map[action], key) + end + end + + return target_map +end + + +local function normalize(map) + local result = {} + for action, keys in pairs(map) do + local uniq = {} + local r = { combination = {}, action = action } + for _, v in ipairs(keys) do + if not uniq[v] then + uniq[v] = true + r.combination[#r.combination+1] = v + end + end + result[#result+1] = r + end + table.sort(result, function(a, b) return a.action < b.action end) + return result +end + + +local function process_module(mod, filename) + local map = {} + load_keymap(mod, map) + load_keymap(mod, map, true) + map = normalize(map) + local f = assert(io.open(filename, "wb")) + f:write(dkjson.encode(map, { indent = true })) + f:close() +end + + +print("Warning: this is not guaranteed to work outside lite-xl's own keymap. Proceed with caution") +local LITE_ROOT = arg[1] +if not LITE_ROOT then + error("LITE_ROOT is not given") +end +package.path = package.path .. ";" .. LITE_ROOT .. "/?.lua;" .. LITE_ROOT .. "/?/init.lua" + +-- fix core.command (because we don't want load the entire thing) +package.loaded["core.command"] = {} + +if #arg > 1 then + for i = 2, #arg do + process_module(arg[i], arg[i] .. ".json") + print(string.format("Exported keymap in %q.", arg[i])) + end +else + process_module(nil, "core.keymap.json") + print("Exported the default keymap.") +end diff --git a/scripts/lhelper.sh b/scripts/lhelper.sh new file mode 100644 index 00000000..af6ae158 --- /dev/null +++ b/scripts/lhelper.sh @@ -0,0 +1,75 @@ +#!/bin/bash +set -e + +show_help() { + echo + echo "Usage: $0 " + echo + echo "Available options:" + echo + echo " --debug Debug this script." + echo "-h --help Show this help and exit." + echo "-p --prefix PREFIX Install directory prefix." + echo " Default: '$HOME/.local'." + echo +} + +main() { + local lhelper_prefix="$HOME/.local" + + for i in "$@"; do + case $i in + -h|--help) + show_help + exit 0 + ;; + -p|--prefix) + lhelper_prefix="$2" + echo "LHelper prefix set to: \"${lhelper_prefix}\"" + shift + shift + ;; + --debug) + set -x + shift + ;; + *) + # unknown option + ;; + esac + done + + if [[ -n $1 ]]; then show_help; exit 1; fi + + if [[ ! -f ${lhelper_prefix}/bin/lhelper ]]; then + + git clone https://github.com/franko/lhelper.git + + # FIXME: This should be set in ~/.bash_profile if not using CI + # export PATH="${HOME}/.local/bin:${PATH}" + mkdir -p "${lhelper_prefix}/bin" + pushd lhelper; bash install "${lhelper_prefix}"; popd + + if [[ "$OSTYPE" == "darwin"* ]]; then + CC=clang CXX=clang++ lhelper create lite-xl -n + else + lhelper create lite-xl -n + fi + fi + + # Not using $(lhelper activate lite-xl) to support CI + source "$(lhelper env-source lite-xl)" + + lhelper install freetype2 + lhelper install sdl2 2.0.14-wait-event-timeout-1 + lhelper install pcre2 + + # Help MSYS2 to find the SDL2 include and lib directories to avoid errors + # during build and linking when using lhelper. + if [[ "$OSTYPE" == "msys" ]]; then + CFLAGS=-I${LHELPER_ENV_PREFIX}/include/SDL2 + LDFLAGS=-L${LHELPER_ENV_PREFIX}/lib + fi +} + +main diff --git a/scripts/meson.build b/scripts/meson.build new file mode 100644 index 00000000..8b45814d --- /dev/null +++ b/scripts/meson.build @@ -0,0 +1,8 @@ +if host_machine.system() == 'windows' + configure_file( + input : 'innosetup/innosetup.iss.in', + output : 'innosetup.iss', + configuration : conf_data + ) +endif + diff --git a/scripts/package.sh b/scripts/package.sh new file mode 100644 index 00000000..1370aee8 --- /dev/null +++ b/scripts/package.sh @@ -0,0 +1,259 @@ +#!/bin/bash +set -e + +if [ ! -e "src/api/api.h" ]; then + echo "Please run this script from the root directory of Lite XL."; exit 1 +fi + +source scripts/common.sh + +show_help() { + echo + echo "Usage: $0 " + echo + echo "Available options:" + echo + echo "-b --builddir DIRNAME Sets the name of the build directory (not path)." + echo " Default: '$(get_default_build_dir)'." + echo "-d --destdir DIRNAME Set the name of the package directory (not path)." + echo " Default: 'lite-xl'." + echo "-h --help Show this help and exit." + echo "-p --prefix PREFIX Install directory prefix. Default: '/'." + echo "-v --version VERSION Sets the version on the package name." + echo " --addons Install 3rd party addons (currently RXI colors)." + echo " --debug Debug this script." + echo "-A --appimage Create an AppImage (Linux only)." + echo "-B --binary Create a normal / portable package or macOS bundle," + echo " depending on how the build was configured. (Default.)" + echo "-D --dmg Create a DMG disk image with AppDMG (macOS only)." + echo "-I --innosetup Create a InnoSetup package (Windows only)." + echo "-S --source Create a source code package," + echo " including subprojects dependencies." + echo +} + +# Addons installation: some distributions forbid external downloads +# so make it as optional module. +install_addons() { + local build_dir="$1" + local data_dir="$2" + + if [[ -d "${build_dir}/third/data/colors" ]]; then + echo "Warning: found previous colors addons installation, skipping." + return 0 + fi + + # Copy third party color themes + curl --insecure \ + -L "https://github.com/rxi/lite-colors/archive/master.zip" \ + -o "${build_dir}/rxi-lite-colors.zip" + + mkdir -p "${build_dir}/third/data/colors" + unzip "${build_dir}/rxi-lite-colors.zip" -d "${build_dir}" + mv "${build_dir}/lite-colors-master/colors" "${build_dir}/third/data" + rm -rf "${build_dir}/lite-colors-master" + + for module_name in colors; do + cp -r "${build_dir}/third/data/$module_name" "${data_dir}" + done +} + +source_package() { + local build_dir=build-src + local package_name=$1 + + rm -rf ${build_dir} + rm -rf ${package_name} + rm -f ${package_name}.tar.gz + + meson subprojects download + meson setup ${build_dir} -Dsource-only=true + + # Note: not using git-archive(-all) because it can't include subprojects ignored by git + rsync -arv \ + --exclude /*build*/ \ + --exclude *.git* \ + --exclude lhelper \ + --exclude lite-xl* \ + --exclude submodules \ + . ${package_name} + + cp "${build_dir}/start.lua" "${package_name}/data/core" + + tar rf ${package_name}.tar ${package_name} + gzip -9 ${package_name}.tar +} + +main() { + local arch="$(uname -m)" + local platform="$(get_platform_name)" + local build_dir="$(get_default_build_dir)" + local dest_dir=lite-xl + local prefix=/ + local version + local addons=false + local appimage=false + local binary=false + local dmg=false + local innosetup=false + local source=false + + for i in "$@"; do + case $i in + -b|--builddir) + build_dir="$2" + shift + shift + ;; + -d|--destdir) + dest_dir="$2" + shift + shift + ;; + -h|--help) + show_help + exit 0 + ;; + -p|--prefix) + prefix="$2" + shift + shift + ;; + -v|--version) + if [[ -n $2 ]]; then version="-$2"; fi + shift + shift + ;; + -A|--appimage) + if [[ "$platform" != "linux" ]]; then + echo "Warning: ignoring --appimage option, works only under Linux." + else + appimage=true + fi + shift + ;; + -B|--binary) + binary=true + shift + ;; + -D|--dmg) + if [[ "$platform" != "macos" ]]; then + echo "Warning: ignoring --dmg option, works only under macOS." + else + dmg=true + fi + shift + ;; + -I|--innosetup) + if [[ "$platform" != "windows" ]]; then + echo "Warning: ignoring --innosetup option, works only under Windows." + else + innosetup=true + fi + shift + ;; + -S|--source) + source=true + shift + ;; + --addons) + addons=true + shift + ;; + --debug) + set -x + shift + ;; + *) + # unknown option + ;; + esac + done + + if [[ -n $1 ]]; then show_help; exit 1; fi + + # The source package doesn't require a previous build, + # nor the following install step, so run it now. + if [[ $source == true ]]; then source_package "lite-xl$version-src"; fi + + # No packages request + if [[ $appimage == false && $binary == false && $dmg == false && $innosetup == false ]]; then + # Source only, return. + if [[ $source == true ]]; then return 0; fi + # Build the binary package as default instead doing nothing. + binary=true + fi + + rm -rf "${dest_dir}" + + DESTDIR="$(pwd)/${dest_dir}" meson install -C "${build_dir}" + + local data_dir="$(pwd)/${dest_dir}/data" + local exe_file="$(pwd)/${dest_dir}/lite-xl" + local package_name=lite-xl$version-$platform-$arch + local bundle=false + local portable=false + local stripcmd="strip" + + if [[ -d "${data_dir}" ]]; then + echo "Creating a portable, compressed archive..." + portable=true + exe_file="$(pwd)/${dest_dir}/lite-xl" + if [[ $platform == "windows" ]]; then + exe_file="${exe_file}.exe" + stripcmd="strip --strip-all" + else + # Windows archive is always portable + package_name+="-portable" + fi + elif [[ $platform == "macos" && ! -d "${data_dir}" ]]; then + data_dir="$(pwd)/${dest_dir}/Contents/Resources" + if [[ -d "${data_dir}" ]]; then + echo "Creating a macOS bundle application..." + bundle=true + # Specify "bundle" on compressed archive only, implicit on images + if [[ $dmg == false ]]; then package_name+="-bundle"; fi + rm -rf "Lite XL.app"; mv "${dest_dir}" "Lite XL.app" + dest_dir="Lite XL.app" + exe_file="$(pwd)/${dest_dir}/Contents/MacOS/lite-xl" + fi + fi + + if [[ $bundle == false && $portable == false ]]; then + echo "Creating a compressed archive..." + data_dir="$(pwd)/${dest_dir}/$prefix/share/lite-xl" + exe_file="$(pwd)/${dest_dir}/$prefix/bin/lite-xl" + fi + + mkdir -p "${data_dir}" + + if [[ $addons == true ]]; then install_addons "${build_dir}" "${data_dir}"; fi + + # TODO: use --skip-subprojects when 0.58.0 will be available on supported + # distributions to avoid subprojects' include and lib directories to be copied. + # Install Meson with PIP to get the latest version is not always possible. + pushd "${dest_dir}" + find . -type d -name 'include' -prune -exec rm -rf {} \; + find . -type d -name 'lib' -prune -exec rm -rf {} \; + find . -type d -empty -delete + popd + + $stripcmd "${exe_file}" + + if [[ $binary == true ]]; then + rm -f "${package_name}".tar.gz + rm -f "${package_name}".zip + + if [[ $platform == "windows" ]]; then + zip -9rv ${package_name}.zip ${dest_dir}/* + else + tar czvf "${package_name}".tar.gz "${dest_dir}" + fi + fi + + if [[ $appimage == true ]]; then source scripts/appimage.sh; fi + if [[ $bundle == true && $dmg == true ]]; then source scripts/appdmg.sh "${package_name}"; fi + if [[ $innosetup == true ]]; then source scripts/innosetup/innosetup.sh -b "${build_dir}"; fi +} + +main "$@" diff --git a/scripts/repackage.sh b/scripts/repackage.sh index 99368582..f8da579f 100644 --- a/scripts/repackage.sh +++ b/scripts/repackage.sh @@ -10,7 +10,7 @@ copy_directory_from_repo () { fi local dirname="$1" local destdir="$2" - git archive master "$dirname" --format=tar | tar xf - -C "$destdir" "${tar_options[@]}" + git archive "$lite_branch" "$dirname" --format=tar | tar xf - -C "$destdir" "${tar_options[@]}" } lite_copy_third_party_modules () { @@ -23,12 +23,17 @@ lite_copy_third_party_modules () { rm "$build/rxi-lite-colors.zip" } +lite_branch=master while [ ! -z ${1+x} ]; do case "$1" in -dir) use_dir="$(realpath $2)" shift 2 ;; + -branch) + lite_branch="$2" + shift 2 + ;; *) echo "unknown option: $1" exit 1 @@ -73,6 +78,8 @@ for filename in $(ls -1 *.zip *.tar.*); do fi rm "$filename" find lite-xl -name lite -exec chmod a+x '{}' \; + start_file=$(find lite-xl -name start.lua) + lite_version=$(cat "$start_file" | awk 'match($0, /^\s*VERSION\s*=\s*"(.+)"/, a) { print(a[1]) }') xcoredir="$(find lite-xl -type d -name 'core')" coredir="$(dirname $xcoredir)" echo "coredir: $coredir" @@ -81,6 +88,7 @@ for filename in $(ls -1 *.zip *.tar.*); do rm -fr "$coredir/$module_name" (cd .. && copy_directory_from_repo --strip-components=1 "data/$module_name" "$workdir/$coredir") done + sed -i "s/@PROJECT_VERSION@/$lite_version/g" "$start_file" for module_name in plugins colors; do cp -r "third/data/$module_name" "$coredir" done diff --git a/scripts/run-local b/scripts/run-local index d5de23aa..8a32e7fa 100755 --- a/scripts/run-local +++ b/scripts/run-local @@ -47,10 +47,7 @@ if [[ "$OSTYPE" == "msys"* || "$OSTYPE" == "mingw"* ]]; then fi rundir=".run" -if [[ "$OSTYPE" == "darwin"* ]]; then - bindir="$rundir" - datadir="$rundir" -elif [ "$option_portable" == on ]; then +if [ "$option_portable" == on ]; then bindir="$rundir" datadir="$rundir/data" else @@ -75,9 +72,14 @@ copy_lite_build () { else cp "$builddir/src/lite-xl" "$bindir" fi + mkdir -p "$datadir/core" for module_name in core plugins colors fonts; do cp -r "data/$module_name" "$datadir" done + # The start.lua file is generated by meson in $builddir but + # there is already a start.lua file in data/core so the command below + # should be executed after we copy the data/core directory. + cp "$builddir/start.lua" "$datadir/core" } run_lite () { diff --git a/src/api/api.c b/src/api/api.c index c479ca4a..9f74f6b5 100644 --- a/src/api/api.c +++ b/src/api/api.c @@ -6,7 +6,6 @@ int luaopen_renderer(lua_State *L); int luaopen_regex(lua_State *L); int luaopen_process(lua_State *L); - static const luaL_Reg libs[] = { { "system", luaopen_system }, { "renderer", luaopen_renderer }, @@ -16,7 +15,6 @@ static const luaL_Reg libs[] = { }; void api_load_libs(lua_State *L) { - for (int i = 0; libs[i].name; i++) { + for (int i = 0; libs[i].name; i++) luaL_requiref(L, libs[i].name, libs[i].func, 1); - } } diff --git a/src/api/api.h b/src/api/api.h index 51ebb9a8..2e9bdb2e 100644 --- a/src/api/api.h +++ b/src/api/api.h @@ -1,12 +1,11 @@ #ifndef API_H #define API_H -#include "lua.h" -#include "lauxlib.h" -#include "lualib.h" +#include +#include +#include #define API_TYPE_FONT "Font" -#define API_TYPE_REPLACE "Replace" #define API_TYPE_PROCESS "Process" void api_load_libs(lua_State *L); diff --git a/src/api/cp_replace.c b/src/api/cp_replace.c deleted file mode 100644 index a0fb3ac8..00000000 --- a/src/api/cp_replace.c +++ /dev/null @@ -1,42 +0,0 @@ -#include "api.h" -#include "renderer.h" - - -static int f_new(lua_State *L) { - CPReplaceTable *rep_table = lua_newuserdata(L, sizeof(CPReplaceTable)); - luaL_setmetatable(L, API_TYPE_REPLACE); - ren_cp_replace_init(rep_table); - return 1; -} - - -static int f_gc(lua_State *L) { - CPReplaceTable *rep_table = luaL_checkudata(L, 1, API_TYPE_REPLACE); - ren_cp_replace_free(rep_table); - return 0; -} - - -static int f_add(lua_State *L) { - CPReplaceTable *rep_table = luaL_checkudata(L, 1, API_TYPE_REPLACE); - const char *src = luaL_checkstring(L, 2); - const char *dst = luaL_checkstring(L, 3); - ren_cp_replace_add(rep_table, src, dst); - return 0; -} - - -static const luaL_Reg lib[] = { - { "__gc", f_gc }, - { "new", f_new }, - { "add", f_add }, - { NULL, NULL } -}; - -int luaopen_renderer_replacements(lua_State *L) { - luaL_newmetatable(L, API_TYPE_REPLACE); - luaL_setfuncs(L, lib, 0); - lua_pushvalue(L, -1); - lua_setfield(L, -2, "__index"); - return 1; -} diff --git a/src/api/process.c b/src/api/process.c index 84d86b6e..4b018e4c 100644 --- a/src/api/process.c +++ b/src/api/process.c @@ -10,28 +10,166 @@ #include #include "api.h" -#define READ_BUF_SIZE 4096 +#define READ_BUF_SIZE 2048 + +#define L_GETTABLE(L, idx, key, conv, def) ( \ + lua_getfield(L, idx, key), \ + conv(L, -1, def) \ +) + +#define L_GETNUM(L, idx, key, def) L_GETTABLE(L, idx, key, luaL_optnumber, def) +#define L_GETSTR(L, idx, key, def) L_GETTABLE(L, idx, key, luaL_optstring, def) + +#define L_SETNUM(L, idx, key, n) (lua_pushnumber(L, n), lua_setfield(L, idx - 1, key)) + +#define L_RETURN_REPROC_ERROR(L, code) { \ + lua_pushnil(L); \ + lua_pushstring(L, reproc_strerror(code)); \ + lua_pushnumber(L, code); \ + return 3; \ +} + +#define ASSERT_MALLOC(ptr) \ + if (ptr == NULL) \ + L_RETURN_REPROC_ERROR(L, REPROC_ENOMEM) + +#define ASSERT_REPROC_ERRNO(L, code) { \ + if (code < 0) \ + L_RETURN_REPROC_ERROR(L, code) \ +} typedef struct { reproc_t * process; - lua_State* L; - + bool running; + int returncode; } process_t; -static int process_new(lua_State* L) +// this function should be called instead of reproc_wait +static int poll_process(process_t* proc, int timeout) { - process_t* self = (process_t*) lua_newuserdata( - L, sizeof(process_t) + int ret = reproc_wait(proc->process, timeout); + if (ret != REPROC_ETIMEDOUT) { + proc->running = false; + proc->returncode = ret; + } + return ret; +} + +static int kill_process(process_t* proc) +{ + int ret = reproc_stop( + proc->process, + (reproc_stop_actions) { + {REPROC_STOP_KILL, 0}, + {REPROC_STOP_TERMINATE, 0}, + {REPROC_STOP_NOOP, 0} + } ); - memset(self, 0, sizeof(process_t)); + if (ret != REPROC_ETIMEDOUT) { + proc->running = false; + proc->returncode = ret; + } - self->process = NULL; - self->L = L; + return ret; +} - luaL_getmetatable(L, API_TYPE_PROCESS); - lua_setmetatable(L, -2); +static int process_start(lua_State* L) +{ + luaL_checktype(L, 1, LUA_TTABLE); + if (lua_isnoneornil(L, 2)) { + lua_settop(L, 1); // remove the nil if it's there + lua_newtable(L); + } + luaL_checktype(L, 2, LUA_TTABLE); + int cmd_len = lua_rawlen(L, 1); + const char** cmd = malloc(sizeof(char *) * (cmd_len + 1)); + ASSERT_MALLOC(cmd); + cmd[cmd_len] = NULL; + + for(int i = 0; i < cmd_len; i++) { + lua_rawgeti(L, 1, i + 1); + + cmd[i] = luaL_checkstring(L, -1); + lua_pop(L, 1); + } + + int deadline = L_GETNUM(L, 2, "timeout", 0); + const char* cwd =L_GETSTR(L, 2, "cwd", NULL); + int redirect_in = L_GETNUM(L, 2, "stdin", REPROC_REDIRECT_DEFAULT); + int redirect_out = L_GETNUM(L, 2, "stdout", REPROC_REDIRECT_DEFAULT); + int redirect_err = L_GETNUM(L, 2, "stderr", REPROC_REDIRECT_DEFAULT); + lua_pop(L, 5); // remove args we just read + + if ( + redirect_in > REPROC_REDIRECT_STDOUT + || redirect_out > REPROC_REDIRECT_STDOUT + || redirect_err > REPROC_REDIRECT_STDOUT) + { + lua_pushnil(L); + lua_pushliteral(L, "redirect to handles, FILE* and paths are not supported"); + return 2; + } + + // env + luaL_getsubtable(L, 2, "env"); + const char **env = NULL; + int env_len = 0; + + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + env_len++; + lua_pop(L, 1); + } + + if (env_len > 0) { + env = malloc(sizeof(char*) * (env_len + 1)); + env[env_len] = NULL; + + int i = 0; + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + lua_pushliteral(L, "="); + lua_pushvalue(L, -3); // push the key to the top + lua_concat(L, 3); // key=value + + env[i++] = luaL_checkstring(L, -1); + lua_pop(L, 1); + } + } + + reproc_t* proc = reproc_new(); + int out = reproc_start( + proc, + (const char* const*) cmd, + (reproc_options) { + .working_directory = cwd, + .deadline = deadline, + .nonblocking = true, + .env = { + .behavior = REPROC_ENV_EXTEND, + .extra = env + }, + .redirect = { + .in.type = redirect_in, + .out.type = redirect_out, + .err.type = redirect_err + } + } + ); + + if (out < 0) { + reproc_destroy(proc); + L_RETURN_REPROC_ERROR(L, out); + } + + process_t* self = lua_newuserdata(L, sizeof(process_t)); + self->process = proc; + self->running = true; + + // this is equivalent to using lua_setmetatable() + luaL_setmetatable(L, API_TYPE_PROCESS); return 1; } @@ -39,24 +177,20 @@ static int process_strerror(lua_State* L) { int error_code = luaL_checknumber(L, 1); - if(error_code){ - lua_pushstring( - L, - reproc_strerror(error_code) - ); - } else { + if (error_code < 0) + lua_pushstring(L, reproc_strerror(error_code)); + else lua_pushnil(L); - } - + return 1; } -static int process_gc(lua_State* L) +static int f_gc(lua_State* L) { process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); - if(self->process){ - reproc_kill(self->process); + if(self->process) { + kill_process(self); reproc_destroy(self->process); self->process = NULL; } @@ -64,330 +198,211 @@ static int process_gc(lua_State* L) return 0; } -static int process_start(lua_State* L) +static int f_tostring(lua_State* L) { - process_t* self = (process_t*) lua_touserdata(L, 1); - - luaL_checktype(L, 2, LUA_TTABLE); - - char* path = NULL; - size_t path_len = 0; - - if(lua_type(L, 3) == LUA_TSTRING){ - path = (char*) lua_tolstring(L, 3, &path_len); - } - - size_t deadline = 0; - - if(lua_type(L, 4) == LUA_TNUMBER){ - deadline = lua_tonumber(L, 4); - } - - size_t table_len = luaL_len(L, 2); - char* command[table_len+1]; - command[table_len] = NULL; - - int i; - for(i=1; i<=table_len; i++){ - lua_pushnumber(L, i); - lua_gettable(L, 2); - - command[i-1] = (char*) lua_tostring(L, -1); - - lua_remove(L, -1); - } - - if(self->process){ - reproc_kill(self->process); - reproc_destroy(self->process); - } - - self->process = reproc_new(); - - int out = reproc_start( - self->process, - (const char* const*) command, - (reproc_options){ - .working_directory = path, - .deadline = deadline, - .nonblocking=true, - .redirect.err.type=REPROC_REDIRECT_PIPE - } - ); - - if(out > 0) { - lua_pushboolean(L, 1); - } - else { - reproc_destroy(self->process); - self->process = NULL; - lua_pushnumber(L, out); - } + luaL_checkudata(L, 1, API_TYPE_PROCESS); + lua_pushliteral(L, API_TYPE_PROCESS); return 1; } -static int process_pid(lua_State* L) +static int f_pid(lua_State* L) { - process_t* self = (process_t*) lua_touserdata(L, 1); + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); - if(self->process){ - int id = reproc_pid(self->process); - - if(id > 0){ - lua_pushnumber(L, id); - } else { - lua_pushnumber(L, 0); - } - } else { - lua_pushnumber(L, 0); - } + lua_pushnumber(L, reproc_pid(self->process)); + return 1; +} + +static int f_returncode(lua_State *L) +{ + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); + int ret = poll_process(self, 0); + + if (self->running) + lua_pushnil(L); + else + lua_pushnumber(L, ret); return 1; } static int g_read(lua_State* L, int stream) { - process_t* self = (process_t*) lua_touserdata(L, 1); + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); + unsigned long read_size = luaL_optunsigned(L, 2, READ_BUF_SIZE); - if(self->process){ - int read_size = READ_BUF_SIZE; - if (lua_type(L, 2) == LUA_TNUMBER){ - read_size = (int) lua_tonumber(L, 2); - } + luaL_Buffer b; + uint8_t* buffer = (uint8_t*) luaL_buffinitsize(L, &b, read_size); - int tries = 1; - if (lua_type(L, 3) == LUA_TNUMBER){ - tries = (int) lua_tonumber(L, 3); - } + int out = reproc_read( + self->process, + stream, + buffer, + read_size + ); - int out = 0; - uint8_t buffer[read_size]; + if (out >= 0) + luaL_addsize(&b, out); + luaL_pushresult(&b); - int runs; - for (runs=0; runsprocess, - REPROC_STREAM_OUT, - buffer, - read_size - ); - - if (out >= 0) - break; - } - - if(out == REPROC_EPIPE){ - reproc_kill(self->process); - reproc_destroy(self->process); - self->process = NULL; - - lua_pushnil(L); - } else if(out > 0) { - lua_pushlstring(L, (const char*) buffer, out); - } else { - lua_pushnil(L); - } - } else { - lua_pushnil(L); + if (out == REPROC_EPIPE) { + kill_process(self); + ASSERT_REPROC_ERRNO(L, out); } return 1; } -static int process_read(lua_State* L) +static int f_read_stdout(lua_State* L) { return g_read(L, REPROC_STREAM_OUT); } -static int process_read_errors(lua_State* L) +static int f_read_stderr(lua_State* L) { return g_read(L, REPROC_STREAM_ERR); } -static int process_write(lua_State* L) +static int f_read(lua_State* L) { - process_t* self = (process_t*) lua_touserdata(L, 1); + int stream = luaL_checknumber(L, 2); + lua_remove(L, 2); + if (stream > REPROC_STREAM_ERR) + L_RETURN_REPROC_ERROR(L, REPROC_EINVAL); - if(self->process){ - size_t data_size = 0; - const char* data = luaL_checklstring(L, 2, &data_size); + return g_read(L, stream); +} - int out = 0; +static int f_write(lua_State* L) +{ + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); - out = reproc_write( - self->process, - (uint8_t*) data, - data_size - ); + size_t data_size = 0; + const char* data = luaL_checklstring(L, 2, &data_size); - if(out == REPROC_EPIPE){ - reproc_kill(self->process); - reproc_destroy(self->process); - self->process = NULL; - } - - lua_pushnumber(L, out); - } else { - lua_pushnumber(L, REPROC_EPIPE); + int out = reproc_write( + self->process, + (uint8_t*) data, + data_size + ); + if (out == REPROC_EPIPE) { + kill_process(self); + L_RETURN_REPROC_ERROR(L, out); } + lua_pushnumber(L, out); + return 1; +} + +static int f_close_stream(lua_State* L) +{ + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); + + int stream = luaL_checknumber(L, 2); + int out = reproc_close(self->process, stream); + ASSERT_REPROC_ERRNO(L, out); + + lua_pushboolean(L, 1); + return 1; +} + +static int f_wait(lua_State* L) +{ + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); + + int timeout = luaL_optnumber(L, 2, 0); + + int ret = poll_process(self, timeout); + // negative returncode is also used for signals on POSIX + if (ret == REPROC_ETIMEDOUT) + L_RETURN_REPROC_ERROR(L, ret); + + lua_pushnumber(L, ret); + return 1; +} + +static int f_terminate(lua_State* L) +{ + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); + + int out = reproc_terminate(self->process); + ASSERT_REPROC_ERRNO(L, out); + + poll_process(self, 0); + lua_pushboolean(L, 1); + return 1; } -static int process_close_stream(lua_State* L) +static int f_kill(lua_State* L) { - process_t* self = (process_t*) lua_touserdata(L, 1); + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); - if(self->process){ - size_t stream = luaL_checknumber(L, 2); + int out = reproc_kill(self->process); + ASSERT_REPROC_ERRNO(L, out); - int out = reproc_close(self->process, stream); - - lua_pushnumber(L, out); - } else { - lua_pushnumber(L, REPROC_EINVAL); - } + poll_process(self, 0); + lua_pushboolean(L, 1); return 1; } -static int process_wait(lua_State* L) +static int f_running(lua_State* L) { - process_t* self = (process_t*) lua_touserdata(L, 1); - - if(self->process){ - size_t timeout = luaL_checknumber(L, 2); - - int out = reproc_wait(self->process, timeout); - - if(out >= 0){ - reproc_destroy(self->process); - self->process = NULL; - } - - lua_pushnumber(L, out); - } else { - lua_pushnumber(L, REPROC_EINVAL); - } + process_t* self = (process_t*) luaL_checkudata(L, 1, API_TYPE_PROCESS); + + poll_process(self, 0); + lua_pushboolean(L, self->running); return 1; } -static int process_terminate(lua_State* L) -{ - process_t* self = (process_t*) lua_touserdata(L, 1); - - if(self->process){ - int out = reproc_terminate(self->process); - - if(out < 0){ - lua_pushnumber(L, out); - } else { - reproc_destroy(self->process); - self->process = NULL; - lua_pushboolean(L, 1); - } - } else { - lua_pushnumber(L, REPROC_EINVAL); - } - - return 1; -} - -static int process_kill(lua_State* L) -{ - process_t* self = (process_t*) lua_touserdata(L, 1); - - if(self->process){ - int out = reproc_kill(self->process); - - if(out < 0){ - lua_pushnumber(L, out); - } else { - reproc_destroy(self->process); - self->process = NULL; - lua_pushboolean(L, 1); - } - } else { - lua_pushnumber(L, REPROC_EINVAL); - } - - return 1; -} - -static int process_running(lua_State* L) -{ - process_t* self = (process_t*) lua_touserdata(L, 1); - - if(self->process){ - lua_pushboolean(L, 1); - } else { - lua_pushboolean(L, 0); - } - - return 1; -} - -static const struct luaL_Reg process_methods[] = { - { "__gc", process_gc}, +static const struct luaL_Reg lib[] = { {"start", process_start}, - {"pid", process_pid}, - {"read", process_read}, - {"read_errors", process_read_errors}, - {"write", process_write}, - {"close_stream", process_close_stream}, - {"wait", process_wait}, - {"terminate", process_terminate}, - {"kill", process_kill}, - {"running", process_running}, - {NULL, NULL} -}; - -static const struct luaL_Reg process[] = { - {"new", process_new}, {"strerror", process_strerror}, - {"ERROR_PIPE", NULL}, - {"ERROR_WOULDBLOCK", NULL}, - {"ERROR_TIMEDOUT", NULL}, - {"ERROR_INVALID", NULL}, - {"STREAM_STDIN", NULL}, - {"STREAM_STDOUT", NULL}, - {"STREAM_STDERR", NULL}, - {"WAIT_INFINITE", NULL}, - {"WAIT_DEADLINE", NULL}, + {"__gc", f_gc}, + {"__tostring", f_tostring}, + {"pid", f_pid}, + {"returncode", f_returncode}, + {"read", f_read}, + {"read_stdout", f_read_stdout}, + {"read_stderr", f_read_stderr}, + {"write", f_write}, + {"close_stream", f_close_stream}, + {"wait", f_wait}, + {"terminate", f_terminate}, + {"kill", f_kill}, + {"running", f_running}, {NULL, NULL} }; int luaopen_process(lua_State *L) { luaL_newmetatable(L, API_TYPE_PROCESS); - luaL_setfuncs(L, process_methods, 0); + luaL_setfuncs(L, lib, 0); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); - luaL_newlib(L, process); + // constants + L_SETNUM(L, -1, "ERROR_INVAL", REPROC_EINVAL); + L_SETNUM(L, -1, "ERROR_TIMEDOUT", REPROC_ETIMEDOUT); + L_SETNUM(L, -1, "ERROR_PIPE", REPROC_EPIPE); + L_SETNUM(L, -1, "ERROR_NOMEM", REPROC_ENOMEM); + L_SETNUM(L, -1, "ERROR_WOULDBLOCK", REPROC_EWOULDBLOCK); - lua_pushnumber(L, REPROC_EPIPE); - lua_setfield(L, -2, "ERROR_PIPE"); + L_SETNUM(L, -1, "WAIT_INFINITE", REPROC_INFINITE); + L_SETNUM(L, -1, "WAIT_DEADLINE", REPROC_DEADLINE); - lua_pushnumber(L, REPROC_EWOULDBLOCK); - lua_setfield(L, -2, "ERROR_WOULDBLOCK"); + L_SETNUM(L, -1, "STREAM_STDIN", REPROC_STREAM_IN); + L_SETNUM(L, -1, "STREAM_STDOUT", REPROC_STREAM_OUT); + L_SETNUM(L, -1, "STREAM_STDERR", REPROC_STREAM_ERR); - lua_pushnumber(L, REPROC_ETIMEDOUT); - lua_setfield(L, -2, "ERROR_TIMEDOUT"); - - lua_pushnumber(L, REPROC_EINVAL); - lua_setfield(L, -2, "ERROR_INVALID"); - - lua_pushnumber(L, REPROC_STREAM_IN); - lua_setfield(L, -2, "STREAM_STDIN"); - - lua_pushnumber(L, REPROC_STREAM_OUT); - lua_setfield(L, -2, "STREAM_STDOUT"); - - lua_pushnumber(L, REPROC_STREAM_ERR); - lua_setfield(L, -2, "STREAM_STDERR"); + L_SETNUM(L, -1, "REDIRECT_DEFAULT", REPROC_REDIRECT_DEFAULT); + L_SETNUM(L, -1, "REDIRECT_PIPE", REPROC_REDIRECT_PIPE); + L_SETNUM(L, -1, "REDIRECT_PARENT", REPROC_REDIRECT_PARENT); + L_SETNUM(L, -1, "REDIRECT_DISCARD", REPROC_REDIRECT_DISCARD); + L_SETNUM(L, -1, "REDIRECT_STDOUT", REPROC_REDIRECT_STDOUT); return 1; } diff --git a/src/api/regex.c b/src/api/regex.c index a5d17604..1043b1c5 100644 --- a/src/api/regex.c +++ b/src/api/regex.c @@ -74,11 +74,11 @@ static int f_pcre_match(lua_State *L) { } PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(md); if (ovector[0] > ovector[1]) { - /* We must guard against patterns such as /(?=.\K)/ that use \K in an + /* We must guard against patterns such as /(?=.\K)/ that use \K in an assertion to set the start of a match later than its end. In the editor, we just detect this case and give up. */ luaL_error(L, "regex matching error: \\K was used in an assertion to " - " set the match start after its end"); + " set the match start after its end"); pcre2_match_data_free(md); return 0; } @@ -103,8 +103,8 @@ int luaopen_regex(lua_State *L) { lua_setfield(L, LUA_REGISTRYINDEX, "regex"); lua_pushnumber(L, PCRE2_ANCHORED); lua_setfield(L, -2, "ANCHORED"); - lua_pushnumber(L, PCRE2_ANCHORED) ; - lua_setfield(L, -2, "ENDANCHORED"); + lua_pushnumber(L, PCRE2_ANCHORED) ; + lua_setfield(L, -2, "ENDANCHORED"); lua_pushnumber(L, PCRE2_NOTBOL); lua_setfield(L, -2, "NOTBOL"); lua_pushnumber(L, PCRE2_NOTEOL); diff --git a/src/api/renderer.c b/src/api/renderer.c index 8dc13ada..60256118 100644 --- a/src/api/renderer.c +++ b/src/api/renderer.c @@ -2,6 +2,131 @@ #include "renderer.h" #include "rencache.h" +static int f_font_load(lua_State *L) { + const char *filename = luaL_checkstring(L, 1); + float size = luaL_checknumber(L, 2); + unsigned int font_hinting = FONT_HINTING_SLIGHT, font_style = 0; + bool subpixel = true; + if (lua_gettop(L) > 2 && lua_istable(L, 3)) { + lua_getfield(L, 3, "antialiasing"); + if (lua_isstring(L, -1)) { + const char *antialiasing = lua_tostring(L, -1); + if (antialiasing) { + if (strcmp(antialiasing, "grayscale") == 0) { + subpixel = false; + } else if (strcmp(antialiasing, "subpixel") == 0) { + subpixel = true; + } else { + return luaL_error(L, "error in renderer.font.load, unknown antialiasing option: \"%s\"", antialiasing); + } + } + } + lua_getfield(L, 3, "hinting"); + if (lua_isstring(L, -1)) { + const char *hinting = lua_tostring(L, -1); + if (hinting) { + if (strcmp(hinting, "slight") == 0) { + font_hinting = FONT_HINTING_SLIGHT; + } else if (strcmp(hinting, "none") == 0) { + font_hinting = FONT_HINTING_NONE; + } else if (strcmp(hinting, "full") == 0) { + font_hinting = FONT_HINTING_FULL; + } else { + return luaL_error(L, "error in renderer.font.load, unknown hinting option: \"%s\"", hinting); + } + } + } + lua_getfield(L, 3, "italic"); + if (lua_toboolean(L, -1)) + font_style |= FONT_STYLE_ITALIC; + lua_getfield(L, 3, "bold"); + if (lua_toboolean(L, -1)) + font_style |= FONT_STYLE_BOLD; + lua_getfield(L, 3, "underline"); + if (lua_toboolean(L, -1)) + font_style |= FONT_STYLE_UNDERLINE; + lua_pop(L, 5); + } + RenFont** font = lua_newuserdata(L, sizeof(RenFont*)); + *font = ren_font_load(filename, size, subpixel, font_hinting, font_style); + if (!*font) + return luaL_error(L, "failed to load font"); + luaL_setmetatable(L, API_TYPE_FONT); + return 1; +} + +static bool font_retrieve(lua_State* L, RenFont** fonts, int idx) { + memset(fonts, 0, sizeof(RenFont*)*FONT_FALLBACK_MAX); + if (lua_type(L, 1) != LUA_TTABLE) { + fonts[0] = *(RenFont**)luaL_checkudata(L, idx, API_TYPE_FONT); + return false; + } + int i = 0; + do { + lua_rawgeti(L, idx, i+1); + fonts[i] = !lua_isnil(L, -1) ? *(RenFont**)luaL_checkudata(L, -1, API_TYPE_FONT) : NULL; + lua_pop(L, 1); + } while (fonts[i] && i++ < FONT_FALLBACK_MAX); + return true; +} + +static int f_font_copy(lua_State *L) { + RenFont* fonts[FONT_FALLBACK_MAX]; + bool table = font_retrieve(L, fonts, 1); + float size = lua_gettop(L) >= 2 ? luaL_checknumber(L, 2) : ren_font_group_get_height(fonts); + if (table) { + lua_newtable(L); + luaL_setmetatable(L, API_TYPE_FONT); + } + for (int i = 0; i < FONT_FALLBACK_MAX && fonts[i]; ++i) { + RenFont** font = lua_newuserdata(L, sizeof(RenFont*)); + *font = ren_font_copy(fonts[i], size); + if (!*font) + return luaL_error(L, "failed to copy font"); + luaL_setmetatable(L, API_TYPE_FONT); + if (table) + lua_rawseti(L, -2, i+1); + } + return 1; +} + +static int f_font_group(lua_State* L) { + luaL_checktype(L, 1, LUA_TTABLE); + luaL_setmetatable(L, API_TYPE_FONT); + return 1; +} + +static int f_font_set_tab_size(lua_State *L) { + RenFont* fonts[FONT_FALLBACK_MAX]; font_retrieve(L, fonts, 1); + int n = luaL_checknumber(L, 2); + ren_font_group_set_tab_size(fonts, n); + return 0; +} + +static int f_font_gc(lua_State *L) { + RenFont** self = luaL_checkudata(L, 1, API_TYPE_FONT); + ren_font_free(*self); + return 0; +} + + +static int f_font_get_width(lua_State *L) { + RenFont* fonts[FONT_FALLBACK_MAX]; font_retrieve(L, fonts, 1); + lua_pushnumber(L, ren_font_group_get_width(fonts, luaL_checkstring(L, 2))); + return 1; +} + +static int f_font_get_height(lua_State *L) { + RenFont* fonts[FONT_FALLBACK_MAX]; font_retrieve(L, fonts, 1); + lua_pushnumber(L, ren_font_group_get_height(fonts)); + return 1; +} + +static int f_font_get_size(lua_State *L) { + RenFont* fonts[FONT_FALLBACK_MAX]; font_retrieve(L, fonts, 1); + lua_pushnumber(L, ren_font_group_get_size(fonts)); + return 1; +} static RenColor checkcolor(lua_State *L, int idx, int def) { RenColor color; @@ -71,40 +196,18 @@ static int f_draw_rect(lua_State *L) { return 0; } -static int draw_text_subpixel_impl(lua_State *L, bool draw_subpixel) { - FontDesc *font_desc = luaL_checkudata(L, 1, API_TYPE_FONT); +static int f_draw_text(lua_State *L) { + RenFont* fonts[FONT_FALLBACK_MAX]; + font_retrieve(L, fonts, 1); const char *text = luaL_checkstring(L, 2); - /* The coordinate below will be in subpixel iff draw_subpixel is true. - Otherwise it will be in pixels. */ - int x_subpixel = luaL_checknumber(L, 3); + float x = luaL_checknumber(L, 3); int y = luaL_checknumber(L, 4); RenColor color = checkcolor(L, 5, 255); - - CPReplaceTable *rep_table; - RenColor replace_color; - if (lua_gettop(L) >= 7) { - rep_table = luaL_checkudata(L, 6, API_TYPE_REPLACE); - replace_color = checkcolor(L, 7, 255); - } else { - rep_table = NULL; - replace_color = (RenColor) {0}; - } - - x_subpixel = rencache_draw_text(L, font_desc, 1, text, x_subpixel, y, color, draw_subpixel, rep_table, replace_color); - lua_pushnumber(L, x_subpixel); + x = rencache_draw_text(L, fonts, text, x, y, color); + lua_pushnumber(L, x); return 1; } -static int f_draw_text(lua_State *L) { - return draw_text_subpixel_impl(L, false); -} - - -static int f_draw_text_subpixel(lua_State *L) { - return draw_text_subpixel_impl(L, true); -} - - static const luaL_Reg lib[] = { { "show_debug", f_show_debug }, { "get_size", f_get_size }, @@ -113,19 +216,27 @@ static const luaL_Reg lib[] = { { "set_clip_rect", f_set_clip_rect }, { "draw_rect", f_draw_rect }, { "draw_text", f_draw_text }, - { "draw_text_subpixel", f_draw_text_subpixel }, { NULL, NULL } }; - -int luaopen_renderer_font(lua_State *L); -int luaopen_renderer_replacements(lua_State *L); +static const luaL_Reg fontLib[] = { + { "__gc", f_font_gc }, + { "load", f_font_load }, + { "copy", f_font_copy }, + { "group", f_font_group }, + { "set_tab_size", f_font_set_tab_size }, + { "get_width", f_font_get_width }, + { "get_height", f_font_get_height }, + { "get_size", f_font_get_size }, + { NULL, NULL } +}; int luaopen_renderer(lua_State *L) { luaL_newlib(L, lib); - luaopen_renderer_font(L); + luaL_newmetatable(L, API_TYPE_FONT); + luaL_setfuncs(L, fontLib, 0); + lua_pushvalue(L, -1); + lua_setfield(L, -2, "__index"); lua_setfield(L, -2, "font"); - luaopen_renderer_replacements(L); - lua_setfield(L, -2, "replacements"); return 1; } diff --git a/src/api/renderer_font.c b/src/api/renderer_font.c deleted file mode 100644 index 7c6587ea..00000000 --- a/src/api/renderer_font.c +++ /dev/null @@ -1,158 +0,0 @@ -#include -#include - -#include "api.h" -#include "fontdesc.h" -#include "renderer.h" -#include "rencache.h" - -static int f_load(lua_State *L) { - const char *filename = luaL_checkstring(L, 1); - float size = luaL_checknumber(L, 2); - unsigned int font_options = 0; - if (lua_gettop(L) > 2 && lua_istable(L, 3)) { - lua_getfield(L, 3, "antialiasing"); - if (lua_isstring(L, -1)) { - const char *antialiasing = lua_tostring(L, -1); - if (antialiasing) { - if (strcmp(antialiasing, "grayscale") == 0) { - font_options |= RenFontGrayscale; - } else if (strcmp(antialiasing, "subpixel") == 0) { - font_options |= RenFontSubpixel; - } else { - return luaL_error(L, "error in renderer.font.load, unknown antialiasing option: \"%s\"", antialiasing); - } - } - } - lua_pop(L, 1); - - lua_getfield(L, 3, "hinting"); - if (lua_isstring(L, -1)) { - const char *hinting = lua_tostring(L, -1); - if (hinting) { - if (strcmp(hinting, "slight") == 0) { - font_options |= RenFontHintingSlight; - } else if (strcmp(hinting, "none") == 0) { - font_options |= RenFontHintingNone; - } else if (strcmp(hinting, "full") == 0) { - font_options |= RenFontHintingFull; - } else { - return luaL_error(L, "error in renderer.font.load, unknown hinting option: \"%s\"", hinting); - } - } - } - lua_pop(L, 1); - } - - if (ren_verify_font(filename)) { - luaL_error(L, "failed to load font"); - } - - FontDesc *font_desc = lua_newuserdata(L, font_desc_alloc_size(filename)); - font_desc_init(font_desc, filename, size, font_options); - luaL_setmetatable(L, API_TYPE_FONT); - return 1; -} - - -static int f_copy(lua_State *L) { - FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT); - float size; - if (lua_gettop(L) >= 2) { - size = luaL_checknumber(L, 2); - } else { - size = self->size; - } - FontDesc *new_font_desc = lua_newuserdata(L, font_desc_alloc_size(self->filename)); - font_desc_init(new_font_desc, self->filename, size, self->options); - luaL_setmetatable(L, API_TYPE_FONT); - return 1; -} - - -static int f_set_tab_size(lua_State *L) { - FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT); - int n = luaL_checknumber(L, 2); - font_desc_set_tab_size(self, n); - return 0; -} - - -static int f_gc(lua_State *L) { - FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT); - font_desc_clear(self); - return 0; -} - -static int f_get_width(lua_State *L) { - FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT); - const char *text = luaL_checkstring(L, 2); - /* By calling ren_get_font_width with NULL as third arguments - we will obtain the width in points. */ - int w = ren_get_font_width(self, text, NULL); - lua_pushnumber(L, w); - return 1; -} - - -static int f_subpixel_scale(lua_State *L) { - FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT); - lua_pushnumber(L, ren_get_font_subpixel_scale(self)); - return 1; -} - -static int f_get_width_subpixel(lua_State *L) { - FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT); - const char *text = luaL_checkstring(L, 2); - int subpixel_scale; - /* We need to pass a non-null subpixel_scale pointer to force - subpixel width calculation. */ - lua_pushnumber(L, ren_get_font_width(self, text, &subpixel_scale)); - return 1; -} - - -static int f_get_height(lua_State *L) { - FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT); - lua_pushnumber(L, ren_get_font_height(self) ); - return 1; -} - - -static int f_get_size(lua_State *L) { - FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT); - lua_pushnumber(L, self->size); - return 1; -} - - -static int f_set_size(lua_State *L) { - FontDesc *self = luaL_checkudata(L, 1, API_TYPE_FONT); - float new_size = luaL_checknumber(L, 2); - font_desc_clear(self); - self->size = new_size; - return 0; -} - - -static const luaL_Reg lib[] = { - { "__gc", f_gc }, - { "load", f_load }, - { "copy", f_copy }, - { "set_tab_size", f_set_tab_size }, - { "get_width", f_get_width }, - { "get_width_subpixel", f_get_width_subpixel }, - { "get_height", f_get_height }, - { "subpixel_scale", f_subpixel_scale }, - { "get_size", f_get_size }, - { "set_size", f_set_size }, - { NULL, NULL } -}; - -int luaopen_renderer_font(lua_State *L) { - luaL_newmetatable(L, API_TYPE_FONT); - luaL_setfuncs(L, lib, 0); - lua_pushvalue(L, -1); - lua_setfield(L, -2, "__index"); - return 1; -} diff --git a/src/api/system.c b/src/api/system.c index 2f1bf763..dc87b723 100644 --- a/src/api/system.c +++ b/src/api/system.c @@ -18,22 +18,21 @@ extern SDL_Window *window; static const char* button_name(int button) { switch (button) { - case 1 : return "left"; - case 2 : return "middle"; - case 3 : return "right"; + case SDL_BUTTON_LEFT : return "left"; + case SDL_BUTTON_MIDDLE : return "middle"; + case SDL_BUTTON_RIGHT : return "right"; + case SDL_BUTTON_X1 : return "x"; + case SDL_BUTTON_X2 : return "y"; default : return "?"; } } -static char* key_name(char *dst, int sym) { - strcpy(dst, SDL_GetKeyName(sym)); - char *p = dst; +static void str_tolower(char *p) { while (*p) { *p = tolower(*p); p++; } - return dst; } struct HitTestInfo { @@ -93,6 +92,23 @@ static SDL_HitTestResult SDLCALL hit_test(SDL_Window *window, const SDL_Point *p return SDL_HITTEST_NORMAL; } +static const char *numpad[] = { "end", "down", "pagedown", "left", "", "right", "home", "up", "pageup", "ins", "delete" }; + +static const char *get_key_name(const SDL_Event *e, char *buf) { + SDL_Scancode scancode = e->key.keysym.scancode; + /* Is the scancode from the keypad and the number-lock off? + ** We assume that SDL_SCANCODE_KP_1 up to SDL_SCANCODE_KP_9 and SDL_SCANCODE_KP_0 + ** and SDL_SCANCODE_KP_PERIOD are declared in SDL2 in that order. */ + if (scancode >= SDL_SCANCODE_KP_1 && scancode <= SDL_SCANCODE_KP_1 + 10 && + !(e->key.keysym.mod & KMOD_NUM)) { + return numpad[scancode - SDL_SCANCODE_KP_1]; + } else { + strcpy(buf, SDL_GetKeyName(e->key.keysym.sym)); + str_tolower(buf); + return buf; + } +} + static int f_poll_event(lua_State *L) { char buf[16]; int mx, my, wx, wy; @@ -162,7 +178,7 @@ top: } #endif lua_pushstring(L, "keypressed"); - lua_pushstring(L, key_name(buf, e.key.keysym.sym)); + lua_pushstring(L, get_key_name(&e, buf)); return 2; case SDL_KEYUP: @@ -176,7 +192,7 @@ top: } #endif lua_pushstring(L, "keyreleased"); - lua_pushstring(L, key_name(buf, e.key.keysym.sym)); + lua_pushstring(L, get_key_name(&e, buf)); return 2; case SDL_TEXTINPUT: @@ -577,56 +593,32 @@ static int f_exec(lua_State *L) { return 0; } - static int f_fuzzy_match(lua_State *L) { size_t strLen, ptnLen; const char *str = luaL_checklstring(L, 1, &strLen); const char *ptn = luaL_checklstring(L, 2, &ptnLen); - bool files = false; - if (lua_gettop(L) > 2 && lua_isboolean(L,3)) - files = lua_toboolean(L, 3); - - int score = 0; - int run = 0; - - // Match things *backwards*. This allows for better matching on filenames than the above + // If true match things *backwards*. This allows for better matching on filenames than the above // function. For example, in the lite project, opening "renderer" has lib/font_render/build.sh // as the first result, rather than src/renderer.c. Clearly that's wrong. - if (files) { - const char* strEnd = str + strLen - 1; - const char* ptnEnd = ptn + ptnLen - 1; - while (strEnd >= str && ptnEnd >= ptn) { - while (*strEnd == ' ') { strEnd--; } - while (*ptnEnd == ' ') { ptnEnd--; } - if (tolower(*strEnd) == tolower(*ptnEnd)) { - score += run * 10 - (*strEnd != *ptnEnd); - run++; - ptnEnd--; - } else { - score -= 10; - run = 0; - } - strEnd--; + bool files = lua_gettop(L) > 2 && lua_isboolean(L,3) && lua_toboolean(L, 3); + int score = 0, run = 0, increment = files ? -1 : 1; + const char* strTarget = files ? str + strLen - 1 : str; + const char* ptnTarget = files ? ptn + ptnLen - 1 : ptn; + while (strTarget >= str && ptnTarget >= ptn && *strTarget && *ptnTarget) { + while (strTarget >= str && *strTarget == ' ') { strTarget += increment; } + while (ptnTarget >= ptn && *ptnTarget == ' ') { ptnTarget += increment; } + if (tolower(*strTarget) == tolower(*ptnTarget)) { + score += run * 10 - (*strTarget != *ptnTarget); + run++; + ptnTarget += increment; + } else { + score -= 10; + run = 0; } - if (ptnEnd >= ptn) { return 0; } - } else { - while (*str && *ptn) { - while (*str == ' ') { str++; } - while (*ptn == ' ') { ptn++; } - if (tolower(*str) == tolower(*ptn)) { - score += run * 10 - (*str != *ptn); - run++; - ptn++; - } else { - score -= 10; - run = 0; - } - str++; - } - if (*ptn) { return 0; } + strTarget += increment; } - - lua_pushnumber(L, score - (int)strLen); + if (ptnTarget >= ptn && *ptnTarget) { return 0; } + lua_pushnumber(L, score - (int)strLen * 10); return 1; } @@ -637,6 +629,86 @@ static int f_set_window_opacity(lua_State *L) { return 1; } +// Symbol table for native plugin loading. Allows for a statically +// bound lua library to be used by native plugins. +typedef struct { + const char* symbol; + void* address; +} lua_function_node; + +#define P(FUNC) { "lua_" #FUNC, (void*)(lua_##FUNC) } +#define U(FUNC) { "luaL_" #FUNC, (void*)(luaL_##FUNC) } +static void* api_require(const char* symbol) { + static lua_function_node nodes[] = { + P(absindex), P(arith), P(atpanic), P(callk), P(checkstack), + P(close), P(compare), P(concat), P(copy), P(createtable), P(dump), + P(error), P(gc), P(getallocf), P(getctx), P(getfield), P(getglobal), + P(gethook), P(gethookcount), P(gethookmask), P(getinfo), P(getlocal), + P(getmetatable), P(getstack), P(gettable), P(gettop), P(getupvalue), + P(getuservalue), P(insert), P(isnumber), P(isstring), P(isuserdata), + P(len), P(load), P(newstate), P(newthread), P(newuserdata), P(next), + P(pcallk), P(pushboolean), P(pushcclosure), P(pushfstring), P(pushinteger), + P(pushlightuserdata), P(pushlstring), P(pushnil), P(pushnumber), + P(pushstring), P(pushthread), P(pushunsigned), P(pushvalue), + P(pushvfstring), P(rawequal), P(rawget), P(rawgeti), P(rawgetp), P(rawlen), + P(rawset), P(rawseti), P(rawsetp), P(remove), P(replace), P(resume), + P(setallocf), P(setfield), P(setglobal), P(sethook), P(setlocal), + P(setmetatable), P(settable), P(settop), P(setupvalue), P(setuservalue), + P(status), P(tocfunction), P(tointegerx), P(tolstring), P(toboolean), + P(tonumberx), P(topointer), P(tothread), P(tounsignedx), P(touserdata), + P(type), P(typename), P(upvalueid), P(upvaluejoin), P(version), P(xmove), + P(yieldk), U(checkversion_), U(getmetafield), U(callmeta), U(tolstring), + U(argerror), U(checknumber), U(optnumber), U(checkinteger), + U(checkunsigned), U(checkstack), U(checktype), U(checkany), + U(newmetatable), U(setmetatable), U(testudata), U(checkudata), U(where), + U(error), U(fileresult), U(execresult), U(ref), U(unref), U(loadstring), + U(newstate), U(len), U(setfuncs), U(getsubtable), U(buffinit), + U(prepbuffsize), U(addlstring), U(addstring), U(addvalue), U(pushresult), + U(pushresultsize), U(buffinitsize) + }; + for (int i = 0; i < sizeof(nodes) / sizeof(lua_function_node); ++i) { + if (strcmp(nodes[i].symbol, symbol) == 0) + return nodes[i].address; + } + return NULL; +} + +static int f_load_native_plugin(lua_State *L) { + char entrypoint_name[512]; entrypoint_name[sizeof(entrypoint_name) - 1] = '\0'; + int result; + + const char *name = luaL_checkstring(L, 1); + const char *path = luaL_checkstring(L, 2); + void *library = SDL_LoadObject(path); + if (!library) + return luaL_error(L, "Unable to load %s: %s", name, SDL_GetError()); + + lua_getglobal(L, "package"); + lua_getfield(L, -1, "native_plugins"); + lua_pushlightuserdata(L, library); + lua_setfield(L, -2, name); + lua_pop(L, 1); + + const char *basename = strrchr(name, '.'); + basename = !basename ? name : basename + 1; + snprintf(entrypoint_name, sizeof(entrypoint_name), "luaopen_lite_xl_%s", basename); + int (*ext_entrypoint) (lua_State *L, void*) = SDL_LoadFunction(library, entrypoint_name); + if (!ext_entrypoint) { + snprintf(entrypoint_name, sizeof(entrypoint_name), "luaopen_%s", basename); + int (*entrypoint)(lua_State *L) = SDL_LoadFunction(library, entrypoint_name); + if (!entrypoint) + return luaL_error(L, "Unable to load %s: Can't find %s(lua_State *L, void *XL)", name, entrypoint_name); + result = entrypoint(L); + } else { + result = ext_entrypoint(L, api_require); + } + + if (!result) + return luaL_error(L, "Unable to load %s: entrypoint must return a value", name); + + return result; +} + static const luaL_Reg lib[] = { { "poll_event", f_poll_event }, @@ -664,6 +736,7 @@ static const luaL_Reg lib[] = { { "exec", f_exec }, { "fuzzy_match", f_fuzzy_match }, { "set_window_opacity", f_set_window_opacity }, + { "load_native_plugin", f_load_native_plugin }, { NULL, NULL } }; diff --git a/src/bundle_open.m b/src/bundle_open.m index f4f0b94c..2ba10da7 100644 --- a/src/bundle_open.m +++ b/src/bundle_open.m @@ -1,31 +1,15 @@ #import #include "lua.h" +#ifdef MACOS_USE_BUNDLE void set_macos_bundle_resources(lua_State *L) { @autoreleasepool { - /* Use resolved executablePath instead of resourcePath to allow lanching - the lite-xl binary via a symlink, like typically done by Homebrew: - - /usr/local/bin/lite-xl -> /Applications/lite-xl.app/Contents/MacOS/lite-xl - - The resourcePath returns /usr/local in this case instead of - /Applications/lite-xl.app/Contents/Resources, which makes later - access to the resource files fail. Resolving the symlink to the - executable and then the relative path to the expected directory - Resources is a workaround for starting the application from both - the launcher directly and the command line via the symlink. - */ - NSString* executable_path = [[NSBundle mainBundle] executablePath]; - char resolved_path[PATH_MAX + 16 + 1]; - realpath([executable_path UTF8String], resolved_path); - strcat(resolved_path, "/../../Resources"); - char resource_path[PATH_MAX + 1]; - realpath(resolved_path, resource_path); - lua_pushstring(L, resource_path); + NSString* resource_path = [[NSBundle mainBundle] resourcePath]; + lua_pushstring(L, [resource_path UTF8String]); lua_setglobal(L, "MACOS_RESOURCES"); }} - +#endif /* Thanks to mathewmariani, taken from his lite-macos github repository. */ void enable_momentum_scroll() { diff --git a/src/fontdesc.c b/src/fontdesc.c deleted file mode 100644 index 44460a6d..00000000 --- a/src/fontdesc.c +++ /dev/null @@ -1,77 +0,0 @@ -#include -#include - -#include "fontdesc.h" -#include "renderer.h" - - -int font_desc_alloc_size(const char *filename) { - return offsetof(FontDesc, filename) + strlen(filename) + 1; -} - -void font_desc_init(FontDesc *font_desc, const char *filename, float size, unsigned int font_options) { - memcpy(font_desc->filename, filename, strlen(filename) + 1); - font_desc->size = size; - font_desc->options = font_options; - font_desc->tab_size = 4; - font_desc->cache_length = 0; - font_desc->cache_last_index = 0; /* Normally no need to initialize. */ -} - -void font_desc_clear(FontDesc *font_desc) { - for (int i = 0; i < font_desc->cache_length; i++) { - ren_free_font(font_desc->cache[i].font); - } - font_desc->cache_length = 0; - font_desc->cache_last_index = 0; -} - -void font_desc_set_tab_size(FontDesc *font_desc, int tab_size) { - font_desc->tab_size = tab_size; - for (int i = 0; i < font_desc->cache_length; i++) { - ren_set_font_tab_size(font_desc->cache[i].font, tab_size); - } -} - -int font_desc_get_tab_size(FontDesc *font_desc) { - return font_desc->tab_size; -} - -static void load_scaled_font(FontDesc *font_desc, int index, int scale) { - RenFont *font = ren_load_font(font_desc->filename, scale * font_desc->size, font_desc->options); - if (!font) { - /* The font was able to load when initially loaded using renderer.load.font. - If now is no longer available we just abort the application. */ - fprintf(stderr, "Fatal error: unable to load font %s. Application will abort.\n", - font_desc->filename); - exit(1); - } - font_desc->cache[index].font = font; - font_desc->cache[index].scale = scale; -} - -RenFont *font_desc_get_font_at_scale(FontDesc *font_desc, int scale) { - int index = -1; - for (int i = 0; i < font_desc->cache_length; i++) { - if (font_desc->cache[i].scale == scale) { - index = i; - break; - } - } - if (index < 0) { - index = font_desc->cache_length; - if (index < FONT_CACHE_ARRAY_MAX) { - load_scaled_font(font_desc, index, scale); - font_desc->cache_length = index + 1; - } else { - // FIXME: should not print into the stderr or stdout. - fprintf(stderr, "Warning: max array of font scale reached.\n"); - index = (font_desc->cache_last_index == 0 ? 1 : 0); - ren_free_font(font_desc->cache[index].font); - load_scaled_font(font_desc, index, scale); - } - } - font_desc->cache_last_index = index; - return font_desc->cache[index].font; -} - diff --git a/src/fontdesc.h b/src/fontdesc.h deleted file mode 100644 index bf591801..00000000 --- a/src/fontdesc.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef FONT_DESC_H -#define FONT_DESC_H - -typedef struct RenFont RenFont; - -struct FontInstance { - RenFont *font; - short int scale; -}; -typedef struct FontInstance FontInstance; - -#define FONT_CACHE_ARRAY_MAX 2 - -struct FontDesc { - float size; - unsigned int options; - short int tab_size; - FontInstance cache[FONT_CACHE_ARRAY_MAX]; - short int cache_length; - short int cache_last_index; /* More recently used instance. */ - char filename[0]; -}; -typedef struct FontDesc FontDesc; - -void font_desc_init(FontDesc *font_desc, const char *filename, float size, unsigned int font_options); -int font_desc_alloc_size(const char *filename); -int font_desc_get_tab_size(FontDesc *font_desc); -void font_desc_set_tab_size(FontDesc *font_desc, int tab_size); -void font_desc_clear(FontDesc *font_desc); -RenFont *font_desc_get_font_at_scale(FontDesc *font_desc, int scale); - -#endif - diff --git a/src/main.c b/src/main.c index 53e8e364..99aa580f 100644 --- a/src/main.c +++ b/src/main.c @@ -9,10 +9,6 @@ #include #elif __linux__ #include - #include - #include - #include - #include #include #elif __APPLE__ #include @@ -22,35 +18,12 @@ SDL_Window *window; static double get_scale(void) { -#ifdef _WIN32 +#ifndef __APPLE__ float dpi; - SDL_GetDisplayDPI(0, NULL, &dpi, NULL); - return dpi / 96.0; -#elif __linux__ - SDL_SysWMinfo info; - XrmDatabase db; - XrmValue value; - char *type = NULL; - - SDL_VERSION(&info.version); - if (!SDL_GetWindowWMInfo(window, &info) - || info.subsystem != SDL_SYSWM_X11) - return 1.0; - - char *resource = XResourceManagerString(info.info.x11.display); - if (resource == NULL) - return 1.0; - - XrmInitialize(); - db = XrmGetStringDatabase(resource); - if (XrmGetResource(db, "Xft.dpi", "String", &type, &value) == False - || value.addr == NULL) - return 1.0; - - return atof(value.addr) / 96.0; -#else - return 1.0; + if (SDL_GetDisplayDPI(0, NULL, &dpi, NULL) == 0) + return dpi / 96.0; #endif + return 1.0; } @@ -59,8 +32,7 @@ static void get_exe_filename(char *buf, int sz) { int len = GetModuleFileName(NULL, buf, sz - 1); buf[len] = '\0'; #elif __linux__ - char path[512]; - sprintf(path, "/proc/%d/exe", getpid()); + char path[] = "/proc/self/exe"; int len = readlink(path, buf, sz - 1); buf[len] = '\0'; #elif __APPLE__ @@ -78,7 +50,7 @@ static void get_exe_filename(char *buf, int sz) { static void init_window_icon(void) { -#ifndef _WIN32 +#if !defined(_WIN32) && !defined(__APPLE__) #include "../resources/icons/icon.inl" (void) icon_rgba_len; /* unused */ SDL_Surface *surf = SDL_CreateRGBSurfaceFrom( @@ -104,8 +76,10 @@ static void init_window_icon(void) { #endif #ifdef __APPLE__ -void set_macos_bundle_resources(lua_State *L); void enable_momentum_scroll(); +#ifdef MACOS_USE_BUNDLE +void set_macos_bundle_resources(lua_State *L); +#endif #endif int main(int argc, char **argv) { @@ -164,8 +138,10 @@ init_lua: lua_setglobal(L, "EXEFILE"); #ifdef __APPLE__ - set_macos_bundle_resources(L); enable_momentum_scroll(); + #ifdef MACOS_USE_BUNDLE + set_macos_bundle_resources(L); + #endif #endif const char *init_lite_code = \ @@ -175,7 +151,7 @@ init_lua: " local exedir = EXEFILE:match('^(.*)" LITE_PATHSEP_PATTERN LITE_NONPATHSEP_PATTERN "$')\n" " local prefix = exedir:match('^(.*)" LITE_PATHSEP_PATTERN "bin$')\n" " dofile((MACOS_RESOURCES or (prefix and prefix .. '/share/lite-xl' or exedir .. '/data')) .. '/core/start.lua')\n" - " core = require('core')\n" + " core = require(os.getenv('LITE_XL_RUNTIME') or 'core')\n" " core.init()\n" " core.run()\n" "end, function(err)\n" @@ -206,6 +182,7 @@ init_lua: lua_pcall(L, 0, 1, 0); if (lua_toboolean(L, -1)) { lua_close(L); + rencache_invalidate(); goto init_lua; } diff --git a/src/meson.build b/src/meson.build index 03cc826f..6fefd257 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,29 +1,33 @@ lite_sources = [ 'api/api.c', - 'api/cp_replace.c', 'api/renderer.c', - 'api/renderer_font.c', 'api/regex.c', 'api/system.c', 'api/process.c', 'renderer.c', 'renwindow.c', - 'fontdesc.c', 'rencache.c', 'main.c', ] -if host_machine.system() == 'darwin' +lite_rc = [] +if host_machine.system() == 'windows' + windows = import('windows') + lite_rc += windows.compile_resources('../resources/icons/icon.rc') +elif host_machine.system() == 'darwin' lite_sources += 'bundle_open.m' endif +lite_include = include_directories('.') + executable('lite-xl', lite_sources + lite_rc, - include_directories: [lite_include, font_renderer_include], + include_directories: [lite_include], dependencies: lite_deps, c_args: lite_cargs, - link_with: libfontrenderer, + objc_args: lite_cargs, link_args: lite_link_args, + install_dir: lite_bindir, install: true, gui_app: true, ) diff --git a/src/rencache.c b/src/rencache.c index 31165e90..c3254cd0 100644 --- a/src/rencache.c +++ b/src/rencache.c @@ -1,6 +1,7 @@ #include #include #include +#include #include #include "rencache.h" @@ -16,32 +17,19 @@ #define COMMAND_BUF_SIZE (1024 * 512) #define COMMAND_BARE_SIZE offsetof(Command, text) -enum { SET_CLIP, DRAW_TEXT, DRAW_RECT, DRAW_TEXT_SUBPIXEL }; +enum { SET_CLIP, DRAW_TEXT, DRAW_RECT }; typedef struct { int8_t type; int8_t tab_size; - int8_t subpixel_scale; - int8_t x_subpixel_offset; int32_t size; RenRect rect; RenColor color; - FontDesc *font_desc; - CPReplaceTable *replacements; - RenColor replace_color; + RenFont *fonts[FONT_FALLBACK_MAX]; + float text_x; char text[0]; } Command; -#define FONT_REFS_MAX 12 -struct FontRef { - FontDesc *font_desc; - int index; -}; -typedef struct FontRef FontRef; -FontRef font_refs[FONT_REFS_MAX]; -int font_refs_len = 0; - - static unsigned cells_buf1[CELLS_X * CELLS_Y]; static unsigned cells_buf2[CELLS_X * CELLS_Y]; static unsigned *cells_prev = cells_buf1; @@ -52,36 +40,9 @@ static int command_buf_idx; static RenRect screen_rect; static bool show_debug; - static inline int min(int a, int b) { return a < b ? a : b; } static inline int max(int a, int b) { return a > b ? a : b; } -static int font_refs_add(lua_State *L, FontDesc *font_desc, int index) { - for (int i = 0; i < font_refs_len; i++) { - if (font_refs[i].font_desc == font_desc) { - return font_refs[i].index; - } - } - - if (font_refs_len >= FONT_REFS_MAX) { - fprintf(stderr, "Warning: (" __FILE__ "): exhausted font reference buffer\n"); - return LUA_NOREF; - } - - lua_pushvalue(L, index); - int ref = luaL_ref(L, LUA_REGISTRYINDEX); - - font_refs[font_refs_len++] = (FontRef) { font_desc, ref }; - return ref; -} - - -static void font_refs_clear(lua_State *L) { - for (int i = 0; i < font_refs_len; i++) { - luaL_unref(L, LUA_REGISTRYINDEX, font_refs[i].index); - } - font_refs_len = 0; -} /* 32bit fnv-1a hash */ #define HASH_INITIAL 2166136261 @@ -124,6 +85,8 @@ static RenRect merge_rects(RenRect a, RenRect b) { static Command* push_command(int type, int size) { + size_t alignment = alignof(max_align_t) - 1; + size = (size + alignment) & ~alignment; Command *cmd = (Command*) (command_buf + command_buf_idx); int n = command_buf_idx + size; if (n > COMMAND_BUF_SIZE) { @@ -168,35 +131,23 @@ void rencache_draw_rect(RenRect rect, RenColor color) { } } -int rencache_draw_text(lua_State *L, FontDesc *font_desc, int font_index, - const char *text, int x, int y, RenColor color, bool draw_subpixel, - CPReplaceTable *replacements, RenColor replace_color) +float rencache_draw_text(lua_State *L, RenFont **fonts, const char *text, float x, int y, RenColor color) { - int subpixel_scale; - int w_subpixel = ren_get_font_width(font_desc, text, &subpixel_scale); - RenRect rect; - rect.x = (draw_subpixel ? ren_font_subpixel_round(x, subpixel_scale, -1) : x); - rect.y = y; - rect.width = ren_font_subpixel_round(w_subpixel, subpixel_scale, 0); - rect.height = ren_get_font_height(font_desc); - - if (rects_overlap(screen_rect, rect) && font_refs_add(L, font_desc, font_index) >= 0) { + float width = ren_font_group_get_width(fonts, text); + RenRect rect = { x, y, (int)width, ren_font_group_get_height(fonts) }; + if (rects_overlap(screen_rect, rect)) { int sz = strlen(text) + 1; - Command *cmd = push_command(draw_subpixel ? DRAW_TEXT_SUBPIXEL : DRAW_TEXT, COMMAND_BARE_SIZE + sz); + Command *cmd = push_command(DRAW_TEXT, COMMAND_BARE_SIZE + sz); if (cmd) { memcpy(cmd->text, text, sz); cmd->color = color; - cmd->font_desc = font_desc; + memcpy(cmd->fonts, fonts, sizeof(RenFont*)*FONT_FALLBACK_MAX); cmd->rect = rect; - cmd->subpixel_scale = (draw_subpixel ? subpixel_scale : 1); - cmd->x_subpixel_offset = x - subpixel_scale * rect.x; - cmd->tab_size = font_desc_get_tab_size(font_desc); - cmd->replacements = replacements; - cmd->replace_color = replace_color; + cmd->text_x = x; + cmd->tab_size = ren_font_group_get_tab_size(fonts); } } - - return x + (draw_subpixel ? w_subpixel : rect.width); + return x + width; } @@ -214,7 +165,6 @@ void rencache_begin_frame(lua_State *L) { screen_rect.height = h; rencache_invalidate(); } - font_refs_clear(L); } @@ -301,15 +251,8 @@ void rencache_end_frame(lua_State *L) { ren_draw_rect(cmd->rect, cmd->color); break; case DRAW_TEXT: - font_desc_set_tab_size(cmd->font_desc, cmd->tab_size); - ren_draw_text(cmd->font_desc, cmd->text, cmd->rect.x, cmd->rect.y, cmd->color, - cmd->replacements, cmd->replace_color); - break; - case DRAW_TEXT_SUBPIXEL: - font_desc_set_tab_size(cmd->font_desc, cmd->tab_size); - ren_draw_text_subpixel(cmd->font_desc, cmd->text, - cmd->subpixel_scale * cmd->rect.x + cmd->x_subpixel_offset, cmd->rect.y, cmd->color, - cmd->replacements, cmd->replace_color); + ren_font_group_set_tab_size(cmd->fonts, cmd->tab_size); + ren_draw_text(cmd->fonts, cmd->text, cmd->text_x, cmd->rect.y, cmd->color); break; } } diff --git a/src/rencache.h b/src/rencache.h index 1d0f45a6..75bb5051 100644 --- a/src/rencache.h +++ b/src/rencache.h @@ -5,13 +5,13 @@ #include #include "renderer.h" -void rencache_show_debug(bool enable); -void rencache_set_clip_rect(RenRect rect); -void rencache_draw_rect(RenRect rect, RenColor color); -int rencache_draw_text(lua_State *L, FontDesc *font_desc, int font_index, const char *text, int x, int y, RenColor color, - bool draw_subpixel, CPReplaceTable *replacements, RenColor replace_color); -void rencache_invalidate(void); -void rencache_begin_frame(lua_State *L); -void rencache_end_frame(lua_State *L); +void rencache_show_debug(bool enable); +void rencache_set_clip_rect(RenRect rect); +void rencache_draw_rect(RenRect rect, RenColor color); +float rencache_draw_text(lua_State *L, RenFont **font, + const char *text, float x, int y, RenColor color); +void rencache_invalidate(void); +void rencache_begin_frame(lua_State *L); +void rencache_end_frame(lua_State *L); #endif diff --git a/src/renderer.c b/src/renderer.c index cd269687..5aa90ff1 100644 --- a/src/renderer.c +++ b/src/renderer.c @@ -2,37 +2,20 @@ #include #include #include -#include "font_renderer.h" +#include +#include +#include +#include FT_FREETYPE_H + #include "renderer.h" #include "renwindow.h" #define MAX_GLYPHSET 256 -#define REPLACEMENT_CHUNK_SIZE 8 - -struct RenImage { - RenColor *pixels; - int width, height; -}; - -struct GlyphSet { - FR_Bitmap *image; - FR_Bitmap_Glyph_Metrics glyphs[256]; -}; -typedef struct GlyphSet GlyphSet; - -/* The field "padding" below must be there just before GlyphSet *sets[MAX_GLYPHSET] - because the field "sets" can be indexed and writted with an index -1. For this - reason the "padding" field must be there but is never explicitly used. */ -struct RenFont { - GlyphSet *padding; - GlyphSet *sets[MAX_GLYPHSET]; - float size; - int height; - int space_advance; - FR_Renderer *renderer; -}; +#define MAX_LOADABLE_GLYPHSETS 1024 +#define SUBPIXEL_BITMAPS_CACHED 3 static RenWindow window_renderer = {0}; +static FT_Library library; static void* check_alloc(void *ptr) { if (!ptr) { @@ -42,6 +25,29 @@ static void* check_alloc(void *ptr) { return ptr; } +/************************* Fonts *************************/ + +typedef struct { + unsigned short x0, x1, y0, y1, loaded; + short bitmap_left, bitmap_top; + float xadvance; +} GlyphMetric; + +typedef struct { + SDL_Surface* surface; + GlyphMetric metrics[MAX_GLYPHSET]; +} GlyphSet; + +typedef struct RenFont { + FT_Face face; + GlyphSet* sets[SUBPIXEL_BITMAPS_CACHED][MAX_LOADABLE_GLYPHSETS]; + float size, space_advance, tab_advance; + short max_height; + bool subpixel; + ERenFontHinting hinting; + unsigned char style; + char path[0]; +} RenFont; static const char* utf8_to_codepoint(const char *p, unsigned *dst) { unsigned res, n; @@ -59,43 +65,292 @@ static const char* utf8_to_codepoint(const char *p, unsigned *dst) { return p + 1; } - -void ren_cp_replace_init(CPReplaceTable *rep_table) { - rep_table->size = 0; - rep_table->replacements = NULL; -} - - -void ren_cp_replace_free(CPReplaceTable *rep_table) { - free(rep_table->replacements); -} - - -void ren_cp_replace_add(CPReplaceTable *rep_table, const char *src, const char *dst) { - int table_size = rep_table->size; - if (table_size % REPLACEMENT_CHUNK_SIZE == 0) { - CPReplace *old_replacements = rep_table->replacements; - const int new_size = (table_size / REPLACEMENT_CHUNK_SIZE + 1) * REPLACEMENT_CHUNK_SIZE; - rep_table->replacements = malloc(new_size * sizeof(CPReplace)); - if (!rep_table->replacements) { - rep_table->replacements = old_replacements; - return; - } - memcpy(rep_table->replacements, old_replacements, table_size * sizeof(CPReplace)); - free(old_replacements); +static int font_set_load_options(RenFont* font) { + switch (font->hinting) { + case FONT_HINTING_SLIGHT: return FT_LOAD_TARGET_LIGHT | FT_LOAD_FORCE_AUTOHINT; + case FONT_HINTING_FULL: return FT_LOAD_TARGET_NORMAL | FT_LOAD_FORCE_AUTOHINT; + case FONT_HINTING_NONE: return FT_LOAD_TARGET_NORMAL | FT_LOAD_NO_HINTING; } - CPReplace *rep = &rep_table->replacements[table_size]; - utf8_to_codepoint(src, &rep->codepoint_src); - utf8_to_codepoint(dst, &rep->codepoint_dst); - rep_table->size = table_size + 1; + return FT_LOAD_TARGET_NORMAL | FT_LOAD_NO_HINTING; } +static int font_set_render_options(RenFont* font) { + if (font->subpixel) { + unsigned char weights[] = { 0x10, 0x40, 0x70, 0x40, 0x10 } ; + switch (font->hinting) { + case FONT_HINTING_NONE: FT_Library_SetLcdFilter(library, FT_LCD_FILTER_NONE); break; + case FONT_HINTING_SLIGHT: + case FONT_HINTING_FULL: FT_Library_SetLcdFilterWeights(library, weights); break; + } + return FT_RENDER_MODE_LCD; + } else { + switch (font->hinting) { + case FONT_HINTING_NONE: return FT_RENDER_MODE_NORMAL; break; + case FONT_HINTING_SLIGHT: return FT_RENDER_MODE_LIGHT; break; + case FONT_HINTING_FULL: return FT_RENDER_MODE_LIGHT; break; + } + } + return 0; +} + +static int font_set_style(FT_Outline* outline, int x_translation, unsigned char style) { + FT_Outline_Translate(outline, x_translation, 0 ); + if (style & FONT_STYLE_BOLD) + FT_Outline_EmboldenXY(outline, 1 << 5, 0); + if (style & FONT_STYLE_ITALIC) { + FT_Matrix matrix = { 1 << 16, 1 << 14, 0, 1 << 16 }; + FT_Outline_Transform(outline, &matrix); + } + return 0; +} + +static void font_load_glyphset(RenFont* font, int idx) { + unsigned int render_option = font_set_render_options(font), load_option = font_set_load_options(font); + int bitmaps_cached = font->subpixel ? SUBPIXEL_BITMAPS_CACHED : 1; + unsigned int byte_width = font->subpixel ? 3 : 1; + for (int j = 0, pen_x = 0; j < bitmaps_cached; ++j) { + GlyphSet* set = check_alloc(calloc(1, sizeof(GlyphSet))); + font->sets[j][idx] = set; + for (int i = 0; i < MAX_GLYPHSET; ++i) { + int glyph_index = FT_Get_Char_Index(font->face, i + idx * MAX_GLYPHSET); + if (!glyph_index || FT_Load_Glyph(font->face, glyph_index, load_option | FT_LOAD_BITMAP_METRICS_ONLY) || font_set_style(&font->face->glyph->outline, j * (64 / SUBPIXEL_BITMAPS_CACHED), font->style) || FT_Render_Glyph(font->face->glyph, render_option)) + continue; + FT_GlyphSlot slot = font->face->glyph; + int glyph_width = slot->bitmap.width / byte_width; + set->metrics[i] = (GlyphMetric){ pen_x, pen_x + glyph_width, 0, slot->bitmap.rows, true, slot->bitmap_left, slot->bitmap_top, (slot->advance.x + slot->lsb_delta - slot->rsb_delta) / 64.0f}; + pen_x += glyph_width; + font->max_height = slot->bitmap.rows > font->max_height ? slot->bitmap.rows : font->max_height; + } + if (pen_x == 0) + continue; + set->surface = check_alloc(SDL_CreateRGBSurface(0, pen_x, font->max_height, font->subpixel ? 24 : 8, 0, 0, 0, 0)); + unsigned char* pixels = set->surface->pixels; + for (int i = 0; i < MAX_GLYPHSET; ++i) { + int glyph_index = FT_Get_Char_Index(font->face, i + idx * MAX_GLYPHSET); + if (!glyph_index || FT_Load_Glyph(font->face, glyph_index, load_option)) + continue; + FT_GlyphSlot slot = font->face->glyph; + font_set_style(&slot->outline, (64 / bitmaps_cached) * j, font->style); + if (FT_Render_Glyph(slot, render_option)) + continue; + for (int line = 0; line < slot->bitmap.rows; ++line) { + int target_offset = set->surface->pitch * line + set->metrics[i].x0 * byte_width; + int source_offset = line * slot->bitmap.pitch; + memcpy(&pixels[target_offset], &slot->bitmap.buffer[source_offset], slot->bitmap.width); + } + } + } +} + +static GlyphSet* font_get_glyphset(RenFont* font, unsigned int codepoint, int subpixel_idx) { + int idx = (codepoint >> 8) % MAX_LOADABLE_GLYPHSETS; + if (!font->sets[font->subpixel ? subpixel_idx : 0][idx]) + font_load_glyphset(font, idx); + return font->sets[font->subpixel ? subpixel_idx : 0][idx]; +} + +static RenFont* font_group_get_glyph(GlyphSet** set, GlyphMetric** metric, RenFont** fonts, unsigned int codepoint, int bitmap_index) { + if (bitmap_index < 0) + bitmap_index += SUBPIXEL_BITMAPS_CACHED; + for (int i = 0; i < FONT_FALLBACK_MAX && fonts[i]; ++i) { + *set = font_get_glyphset(fonts[i], codepoint, bitmap_index); + *metric = &(*set)->metrics[codepoint % 256]; + if ((*metric)->loaded || codepoint < 0xFF) + return fonts[i]; + } + if (!(*metric)->loaded && codepoint > 0xFF && codepoint != 0x25A1) + return font_group_get_glyph(set, metric, fonts, 0x25A1, bitmap_index); + return fonts[0]; +} + +RenFont* ren_font_load(const char* path, float size, bool subpixel, unsigned char hinting, unsigned char style) { + FT_Face face; + if (FT_New_Face( library, path, 0, &face)) + return NULL; + const int surface_scale = renwin_surface_scale(&window_renderer); + if (FT_Set_Pixel_Sizes(face, 0, (int)(size*surface_scale))) + goto failure; + int len = strlen(path); + RenFont* font = check_alloc(calloc(1, sizeof(RenFont) + len + 1)); + strcpy(font->path, path); + font->face = face; + font->size = size; + font->subpixel = subpixel; + font->hinting = hinting; + font->style = style; + font->space_advance = (int)font_get_glyphset(font, ' ', 0)->metrics[' '].xadvance; + font->tab_advance = font->space_advance * 2; + return font; + failure: + FT_Done_Face(face); + return NULL; +} + +RenFont* ren_font_copy(RenFont* font, float size) { + return ren_font_load(font->path, size, font->subpixel, font->hinting, font->style); +} + +void ren_font_free(RenFont* font) { + for (int i = 0; i < SUBPIXEL_BITMAPS_CACHED; ++i) { + for (int j = 0; j < MAX_GLYPHSET; ++j) { + if (font->sets[i][j]) { + if (font->sets[i][j]->surface) + SDL_FreeSurface(font->sets[i][j]->surface); + free(font->sets[i][j]); + } + } + } + FT_Done_Face(font->face); + free(font); +} + +void ren_font_group_set_tab_size(RenFont **fonts, int n) { + for (int j = 0; j < FONT_FALLBACK_MAX && fonts[j]; ++j) { + for (int i = 0; i < (fonts[j]->subpixel ? SUBPIXEL_BITMAPS_CACHED : 1); ++i) + font_get_glyphset(fonts[j], '\t', i)->metrics['\t'].xadvance = fonts[j]->space_advance * n; + } +} + +int ren_font_group_get_tab_size(RenFont **fonts) { + return font_get_glyphset(fonts[0], '\t', 0)->metrics['\t'].xadvance / fonts[0]->space_advance; +} + +float ren_font_group_get_size(RenFont **fonts) { + return fonts[0]->size; +} +int ren_font_group_get_height(RenFont **fonts) { + return fonts[0]->size + 3; +} + +float ren_font_group_get_width(RenFont **fonts, const char *text) { + float width = 0; + const char* end = text + strlen(text); + GlyphMetric* metric = NULL; GlyphSet* set = NULL; + while (text < end) { + unsigned int codepoint; + text = utf8_to_codepoint(text, &codepoint); + font_group_get_glyph(&set, &metric, fonts, codepoint, 0); + width += metric->xadvance ? metric->xadvance : fonts[0]->space_advance; + } + const int surface_scale = renwin_surface_scale(&window_renderer); + return width / surface_scale; +} + +float ren_draw_text(RenFont **fonts, const char *text, float x, int y, RenColor color) { + SDL_Surface *surface = renwin_get_surface(&window_renderer); + const RenRect clip = window_renderer.clip; + + const int surface_scale = renwin_surface_scale(&window_renderer); + float pen_x = x * surface_scale; + y *= surface_scale; + int bytes_per_pixel = surface->format->BytesPerPixel; + const char* end = text + strlen(text); + unsigned char* destination_pixels = surface->pixels; + int clip_end_x = clip.x + clip.width, clip_end_y = clip.y + clip.height; + + while (text < end) { + unsigned int codepoint, r, g, b; + text = utf8_to_codepoint(text, &codepoint); + GlyphSet* set = NULL; GlyphMetric* metric = NULL; + RenFont* font = font_group_get_glyph(&set, &metric, fonts, codepoint, (int)(fmod(pen_x, 1.0) * SUBPIXEL_BITMAPS_CACHED)); + int start_x = floor(pen_x) + metric->bitmap_left; + int end_x = (metric->x1 - metric->x0) + start_x; + int glyph_end = metric->x1, glyph_start = metric->x0; + if (!metric->loaded && codepoint > 0xFF) + ren_draw_rect((RenRect){ start_x + 1, y, font->space_advance - 1, ren_font_group_get_height(fonts) }, color); + if (set->surface && color.a > 0 && end_x >= clip.x && start_x < clip_end_x) { + unsigned char* source_pixels = set->surface->pixels; + for (int line = metric->y0; line < metric->y1; ++line) { + int target_y = line + y - metric->y0 - metric->bitmap_top + font->size * surface_scale; + if (target_y < clip.y) + continue; + if (target_y >= clip_end_y) + break; + if (start_x + (glyph_end - glyph_start) >= clip_end_x) + glyph_end = glyph_start + (clip_end_x - start_x); + else if (start_x < clip.x) { + int offset = clip.x - start_x; + start_x += offset; + glyph_start += offset; + } + unsigned int* destination_pixel = (unsigned int*)&destination_pixels[surface->pitch * target_y + start_x * bytes_per_pixel]; + unsigned char* source_pixel = &source_pixels[line * set->surface->pitch + glyph_start * (font->subpixel ? 3 : 1)]; + for (int x = glyph_start; x < glyph_end; ++x) { + unsigned int destination_color = *destination_pixel; + SDL_Color dst = { (destination_color & surface->format->Rmask) >> surface->format->Rshift, (destination_color & surface->format->Gmask) >> surface->format->Gshift, (destination_color & surface->format->Bmask) >> surface->format->Bshift, (destination_color & surface->format->Amask) >> surface->format->Ashift }; + SDL_Color src = { *(font->subpixel ? source_pixel++ : source_pixel), *(font->subpixel ? source_pixel++ : source_pixel), *source_pixel++ }; + r = (color.r * src.r * color.a + dst.r * (65025 - src.r * color.a) + 32767) / 65025; + g = (color.g * src.g * color.a + dst.g * (65025 - src.g * color.a) + 32767) / 65025; + b = (color.b * src.b * color.a + dst.b * (65025 - src.b * color.a) + 32767) / 65025; + *destination_pixel++ = dst.a << surface->format->Ashift | r << surface->format->Rshift | g << surface->format->Gshift | b << surface->format->Bshift; + } + } + } + pen_x += metric->xadvance ? metric->xadvance : font->space_advance; + } + if (fonts[0]->style & FONT_STYLE_UNDERLINE) + ren_draw_rect((RenRect){ x, y / surface_scale + ren_font_group_get_height(fonts) - 1, (pen_x - x) / surface_scale, 1 }, color); + return pen_x / surface_scale; +} + +/******************* Rectangles **********************/ +static inline RenColor blend_pixel(RenColor dst, RenColor src) { + int ia = 0xff - src.a; + dst.r = ((src.r * src.a) + (dst.r * ia)) >> 8; + dst.g = ((src.g * src.a) + (dst.g * ia)) >> 8; + dst.b = ((src.b * src.a) + (dst.b * ia)) >> 8; + return dst; +} + +void ren_draw_rect(RenRect rect, RenColor color) { + if (color.a == 0) { return; } + + const int surface_scale = renwin_surface_scale(&window_renderer); + + /* transforms coordinates in pixels. */ + rect.x *= surface_scale; + rect.y *= surface_scale; + rect.width *= surface_scale; + rect.height *= surface_scale; + + const RenRect clip = window_renderer.clip; + int x1 = rect.x < clip.x ? clip.x : rect.x; + int y1 = rect.y < clip.y ? clip.y : rect.y; + int x2 = rect.x + rect.width; + int y2 = rect.y + rect.height; + x2 = x2 > clip.x + clip.width ? clip.x + clip.width : x2; + y2 = y2 > clip.y + clip.height ? clip.y + clip.height : y2; + + SDL_Surface *surface = renwin_get_surface(&window_renderer); + RenColor *d = (RenColor*) surface->pixels; + d += x1 + y1 * surface->w; + int dr = surface->w - (x2 - x1); + unsigned int translated = SDL_MapRGB(surface->format, color.r, color.g, color.b); + if (color.a == 0xff) { + SDL_Rect rect = { x1, y1, x2 - x1, y2 - y1 }; + SDL_FillRect(surface, &rect, translated); + } else { + RenColor translated_color = (RenColor){ translated & 0xFF, (translated >> 8) & 0xFF, (translated >> 16) & 0xFF, color.a }; + for (int j = y1; j < y2; j++) { + for (int i = x1; i < x2; i++, d++) + *d = blend_pixel(*d, translated_color); + d += dr; + } + } +} + +/*************** Window Management ****************/ void ren_free_window_resources() { renwin_free(&window_renderer); } void ren_init(SDL_Window *win) { assert(win); + int error = FT_Init_FreeType( &library ); + if ( error ) { + fprintf(stderr, "internal font error when starting the application\n"); + return; + } window_renderer.window = win; renwin_init_surface(&window_renderer); renwin_clip_to_surface(&window_renderer); @@ -130,274 +385,3 @@ void ren_get_size(int *x, int *y) { *y = surface->h / scale; } - -RenImage* ren_new_image(int width, int height) { - assert(width > 0 && height > 0); - RenImage *image = malloc(sizeof(RenImage) + width * height * sizeof(RenColor)); - check_alloc(image); - image->pixels = (void*) (image + 1); - image->width = width; - image->height = height; - return image; -} - -void ren_free_image(RenImage *image) { - free(image); -} - -static GlyphSet* load_glyphset(RenFont *font, int idx) { - GlyphSet *set = check_alloc(calloc(1, sizeof(GlyphSet))); - - set->image = FR_Bake_Font_Bitmap(font->renderer, font->height, idx << 8, 256, set->glyphs); - check_alloc(set->image); - - return set; -} - - -static GlyphSet* get_glyphset(RenFont *font, int codepoint) { - int idx = (codepoint >> 8) % MAX_GLYPHSET; - if (!font->sets[idx]) { - font->sets[idx] = load_glyphset(font, idx); - } - return font->sets[idx]; -} - - -int ren_verify_font(const char *filename) { - RenFont font[1]; - font->renderer = FR_Renderer_New(0); - if (FR_Load_Font(font->renderer, filename)) { - return 1; - } - FR_Renderer_Free(font->renderer); - return 0; -} - - -RenFont* ren_load_font(const char *filename, float size, unsigned int renderer_flags) { - RenFont *font = NULL; - - /* init font */ - font = check_alloc(calloc(1, sizeof(RenFont))); - font->size = size; - - unsigned int fr_renderer_flags = 0; - if ((renderer_flags & RenFontAntialiasingMask) == RenFontSubpixel) { - fr_renderer_flags |= FR_SUBPIXEL; - } - if ((renderer_flags & RenFontHintingMask) == RenFontHintingSlight) { - fr_renderer_flags |= (FR_HINTING | FR_PRESCALE_X); - } else if ((renderer_flags & RenFontHintingMask) == RenFontHintingFull) { - fr_renderer_flags |= FR_HINTING; - } - font->renderer = FR_Renderer_New(fr_renderer_flags); - if (FR_Load_Font(font->renderer, filename)) { - free(font); - return NULL; - } - font->height = FR_Get_Font_Height(font->renderer, size); - - FR_Bitmap_Glyph_Metrics *gs = get_glyphset(font, ' ')->glyphs; - font->space_advance = gs[' '].xadvance; - - /* make tab and newline glyphs invisible */ - FR_Bitmap_Glyph_Metrics *g = get_glyphset(font, '\n')->glyphs; - g['\t'].x1 = g['\t'].x0; - g['\n'].x1 = g['\n'].x0; - - return font; -} - - -void ren_free_font(RenFont *font) { - for (int i = 0; i < MAX_GLYPHSET; i++) { - GlyphSet *set = font->sets[i]; - if (set) { - FR_Bitmap_Free(set->image); - free(set); - } - } - FR_Renderer_Free(font->renderer); - free(font); -} - - -void ren_set_font_tab_size(RenFont *font, int n) { - GlyphSet *set = get_glyphset(font, '\t'); - set->glyphs['\t'].xadvance = font->space_advance * n; -} - - -int ren_get_font_tab_size(RenFont *font) { - GlyphSet *set = get_glyphset(font, '\t'); - return set->glyphs['\t'].xadvance / font->space_advance; -} - - -/* Important: if subpixel_scale is NULL we will return width in points. Otherwise we will - return width in subpixels. */ -int ren_get_font_width(FontDesc *font_desc, const char *text, int *subpixel_scale) { - int x = 0; - const char *p = text; - unsigned codepoint; - const int surface_scale = renwin_surface_scale(&window_renderer); - RenFont *font = font_desc_get_font_at_scale(font_desc, surface_scale); - while (*p) { - p = utf8_to_codepoint(p, &codepoint); - GlyphSet *set = get_glyphset(font, codepoint); - FR_Bitmap_Glyph_Metrics *g = &set->glyphs[codepoint & 0xff]; - x += g->xadvance; - } - /* At this point here x is in subpixel units */ - const int x_scale_to_points = FR_Subpixel_Scale(font->renderer) * surface_scale; - if (subpixel_scale) { - *subpixel_scale = x_scale_to_points; - return x; - } - return (x + x_scale_to_points / 2) / x_scale_to_points; -} - - -int ren_get_font_height(FontDesc *font_desc) { - const int surface_scale = renwin_surface_scale(&window_renderer); - RenFont *font = font_desc_get_font_at_scale(font_desc, surface_scale); - return (font->height + surface_scale / 2) / surface_scale; -} - - -static inline RenColor blend_pixel(RenColor dst, RenColor src) { - int ia = 0xff - src.a; - dst.r = ((src.r * src.a) + (dst.r * ia)) >> 8; - dst.g = ((src.g * src.a) + (dst.g * ia)) >> 8; - dst.b = ((src.b * src.a) + (dst.b * ia)) >> 8; - return dst; -} - - -#define rect_draw_loop(expr) \ - for (int j = y1; j < y2; j++) { \ - for (int i = x1; i < x2; i++) { \ - *d = expr; \ - d++; \ - } \ - d += dr; \ - } - -void ren_draw_rect(RenRect rect, RenColor color) { - if (color.a == 0) { return; } - - const int surface_scale = renwin_surface_scale(&window_renderer); - - /* transforms coordinates in pixels. */ - rect.x *= surface_scale; - rect.y *= surface_scale; - rect.width *= surface_scale; - rect.height *= surface_scale; - - const RenRect clip = window_renderer.clip; - int x1 = rect.x < clip.x ? clip.x : rect.x; - int y1 = rect.y < clip.y ? clip.y : rect.y; - int x2 = rect.x + rect.width; - int y2 = rect.y + rect.height; - x2 = x2 > clip.x + clip.width ? clip.x + clip.width : x2; - y2 = y2 > clip.y + clip.height ? clip.y + clip.height : y2; - - SDL_Surface *surface = renwin_get_surface(&window_renderer); - RenColor *d = (RenColor*) surface->pixels; - d += x1 + y1 * surface->w; - int dr = surface->w - (x2 - x1); - - if (color.a == 0xff) { - SDL_Rect rect = { x1, y1, x2 - x1, y2 - y1 }; - SDL_FillRect(surface, &rect, SDL_MapRGBA(surface->format, color.r, color.g, color.b, color.a)); - } else { - rect_draw_loop(blend_pixel(*d, color)); - } -} - - -static int codepoint_replace(CPReplaceTable *rep_table, unsigned *codepoint) { - for (int i = 0; i < rep_table->size; i++) { - const CPReplace *rep = &rep_table->replacements[i]; - if (*codepoint == rep->codepoint_src) { - *codepoint = rep->codepoint_dst; - return 1; - } - } - return 0; -} - - -static FR_Clip_Area clip_area_from_rect(const RenRect r) { - return (FR_Clip_Area) {r.x, r.y, r.x + r.width, r.y + r.height}; -} - - -static void draw_text_impl(RenFont *font, const char *text, int x_subpixel, int y_pixel, RenColor color, - CPReplaceTable *replacements, RenColor replace_color) -{ - SDL_Surface *surf = renwin_get_surface(&window_renderer); - FR_Clip_Area clip = clip_area_from_rect(window_renderer.clip); - const char *p = text; - unsigned codepoint; - const FR_Color color_fr = { .r = color.r, .g = color.g, .b = color.b }; - while (*p) { - FR_Color color_rep; - p = utf8_to_codepoint(p, &codepoint); - GlyphSet *set = get_glyphset(font, codepoint); - FR_Bitmap_Glyph_Metrics *g = &set->glyphs[codepoint & 0xff]; - const int xadvance_original_cp = g->xadvance; - const int replaced = replacements ? codepoint_replace(replacements, &codepoint) : 0; - if (replaced) { - set = get_glyphset(font, codepoint); - g = &set->glyphs[codepoint & 0xff]; - color_rep = (FR_Color) { .r = replace_color.r, .g = replace_color.g, .b = replace_color.b}; - } else { - color_rep = color_fr; - } - if (color.a != 0) { - FR_Blend_Glyph(font->renderer, &clip, - x_subpixel, y_pixel, (uint8_t *) surf->pixels, surf->w, set->image, g, color_rep); - } - x_subpixel += xadvance_original_cp; - } -} - - -void ren_draw_text_subpixel(FontDesc *font_desc, const char *text, int x_subpixel, int y, RenColor color, - CPReplaceTable *replacements, RenColor replace_color) -{ - const int surface_scale = renwin_surface_scale(&window_renderer); - RenFont *font = font_desc_get_font_at_scale(font_desc, surface_scale); - draw_text_impl(font, text, x_subpixel, surface_scale * y, color, replacements, replace_color); -} - -void ren_draw_text(FontDesc *font_desc, const char *text, int x, int y, RenColor color, - CPReplaceTable *replacements, RenColor replace_color) -{ - const int surface_scale = renwin_surface_scale(&window_renderer); - RenFont *font = font_desc_get_font_at_scale(font_desc, surface_scale); - const int subpixel_scale = surface_scale * FR_Subpixel_Scale(font->renderer); - draw_text_impl(font, text, subpixel_scale * x, surface_scale * y, color, replacements, replace_color); -} - -// Could be declared as static inline -int ren_font_subpixel_round(int width, int subpixel_scale, int orientation) { - int w_mult; - if (orientation < 0) { - w_mult = width; - } else if (orientation == 0) { - w_mult = width + subpixel_scale / 2; - } else { - w_mult = width + subpixel_scale - 1; - } - return w_mult / subpixel_scale; -} - - -int ren_get_font_subpixel_scale(FontDesc *font_desc) { - const int surface_scale = renwin_surface_scale(&window_renderer); - RenFont *font = font_desc_get_font_at_scale(font_desc, surface_scale); - return FR_Subpixel_Scale(font->renderer) * surface_scale; -} diff --git a/src/renderer.h b/src/renderer.h index bab059bb..6058583e 100644 --- a/src/renderer.h +++ b/src/renderer.h @@ -3,37 +3,26 @@ #include #include -#include "fontdesc.h" - -typedef struct RenImage RenImage; - -enum { - RenFontAntialiasingMask = 1, - RenFontGrayscale = 1, - RenFontSubpixel = 0, - - RenFontHintingMask = 3 << 1, - RenFontHintingSlight = 0 << 1, - RenFontHintingNone = 1 << 1, - RenFontHintingFull = 2 << 1, -}; +#include +#define FONT_FALLBACK_MAX 4 +typedef struct RenFont RenFont; +typedef enum { FONT_HINTING_NONE, FONT_HINTING_SLIGHT, FONT_HINTING_FULL } ERenFontHinting; +typedef enum { FONT_STYLE_BOLD = 1, FONT_STYLE_ITALIC = 2, FONT_STYLE_UNDERLINE = 4 } ERenFontStyle; typedef struct { uint8_t b, g, r, a; } RenColor; typedef struct { int x, y, width, height; } RenRect; -struct CPReplace { - unsigned codepoint_src; - unsigned codepoint_dst; -}; -typedef struct CPReplace CPReplace; - - -struct CPReplaceTable { - int size; - CPReplace *replacements; -}; -typedef struct CPReplaceTable CPReplaceTable; +RenFont* ren_font_load(const char *filename, float size, bool subpixel, unsigned char hinting, unsigned char style); +RenFont* ren_font_copy(RenFont* font, float size); +void ren_font_free(RenFont *font); +int ren_font_group_get_tab_size(RenFont **font); +int ren_font_group_get_height(RenFont **font); +float ren_font_group_get_size(RenFont **font); +void ren_font_group_set_tab_size(RenFont **font, int n); +float ren_font_group_get_width(RenFont **font, const char *text); +float ren_draw_text(RenFont **font, const char *text, float x, int y, RenColor color); +void ren_draw_rect(RenRect rect, RenColor color); void ren_init(SDL_Window *win); void ren_resize_window(); @@ -42,27 +31,5 @@ void ren_set_clip_rect(RenRect rect); void ren_get_size(int *x, int *y); /* Reports the size in points. */ void ren_free_window_resources(); -RenImage* ren_new_image(int width, int height); -void ren_free_image(RenImage *image); - -RenFont* ren_load_font(const char *filename, float size, unsigned int renderer_flags); -int ren_verify_font(const char *filename); -void ren_free_font(RenFont *font); -void ren_set_font_tab_size(RenFont *font, int n); -int ren_get_font_tab_size(RenFont *font); - -int ren_get_font_width(FontDesc *font_desc, const char *text, int *subpixel_scale); -int ren_get_font_height(FontDesc *font_desc); -int ren_get_font_subpixel_scale(FontDesc *font_desc); -int ren_font_subpixel_round(int width, int subpixel_scale, int orientation); - -void ren_draw_rect(RenRect rect, RenColor color); -void ren_draw_text(FontDesc *font_desc, const char *text, int x, int y, RenColor color, CPReplaceTable *replacements, RenColor replace_color); -void ren_draw_text_subpixel(FontDesc *font_desc, const char *text, int x_subpixel, int y, RenColor color, CPReplaceTable *replacements, RenColor replace_color); - -void ren_cp_replace_init(CPReplaceTable *rep_table); -void ren_cp_replace_free(CPReplaceTable *rep_table); -void ren_cp_replace_add(CPReplaceTable *rep_table, const char *src, const char *dst); -void ren_cp_replace_clear(CPReplaceTable *rep_table); #endif diff --git a/subprojects/libagg.wrap b/subprojects/libagg.wrap deleted file mode 100644 index d5618583..00000000 --- a/subprojects/libagg.wrap +++ /dev/null @@ -1,4 +0,0 @@ -[wrap-git] -directory = libagg -url = https://github.com/franko/agg -revision = v2.4-lhelper4 diff --git a/subprojects/reproc.wrap b/subprojects/reproc.wrap index f1afb4fa..9ff98b7e 100644 --- a/subprojects/reproc.wrap +++ b/subprojects/reproc.wrap @@ -1,4 +1,4 @@ [wrap-git] directory = reproc url = https://github.com/franko/reproc -revision = v14.2.2-meson-1 +revision = v14.2.3-meson-1