diff --git a/doc/Makefile b/doc/Makefile index 2ffe2a88..b9d5d948 100644 --- a/doc/Makefile +++ b/doc/Makefile @@ -1,154 +1,24 @@ -# Makefile for Sphinx documentation +# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build -PAPER = +SPHINXPROJ = PySDL2 +SOURCEDIR = . BUILDDIR = _build -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext - +# Put it first so that "make" without argument is like "make help". help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - @rm -rf $(BUILDDIR)/* - @rm -f modules/*~ tutorial/*~ *~ - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PySDL2.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PySDL2.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/PyMule" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PyMule" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." +.PHONY: help Makefile view -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." +# View the built documentation +view: + @python -c "import webbrowser; webbrowser.open_new_tab('file://$(PWD)/_build/html/index.html')" diff --git a/doc/conf.py b/doc/conf.py index d914f70f..066df843 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,253 +1,127 @@ # -*- coding: utf-8 -*- -# -# PySDL2 documentation build configuration file, created by -# sphinx-quickstart on Thu Mar 22 07:51:57 2012. -# -# This file is execfile()d with the current directory set to its containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. -import sys, os +import os +import sys +import pkg_resources +from datetime import date + +# -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -# sys.path.insert(0, os.path.abspath('.')) -# -- General configuration ----------------------------------------------------- +parent_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) +sys.path.append(os.path.join(parent_dir, "sdl2", "ext")) + + +# -- Project information ----------------------------------------------------- + +curr_date = date.today() +copyright_str = '{0}, Marcus von Appen. Last updated: {1}' + +project = 'PySDL2' +copyright = copyright_str.format(curr_date.year, curr_date.isoformat()) +author = 'Marcus von Appen & Austin Hurst' + +# The full version, including alpha/beta/rc tags +release = pkg_resources.require("PySDL2")[0].version + +# The short X.Y version +version = release[:3] + + +# -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. +# # needs_sphinx = '1.0' -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.coverage', - 'sphinx.ext.inheritance_diagram' - ] - -todo_include_todos = True -graphviz_output_format = 'png' +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.autosummary', + 'sphinx.ext.napoleon', + 'sphinx.ext.intersphinx', + 'sphinx.ext.viewcode', + 'sphinx.ext.todo', + 'sphinx.ext.inheritance_diagram', +] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] -# The suffix of source filenames. +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] source_suffix = '.rst' -# The encoding of source files. -# source_encoding = 'utf-8-sig' - # The master toctree document. master_doc = 'index' -# General information about the project. -project = u'PySDL2' -copyright = u'2012-2020, Marcus von Appen' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '0.9' -# The full version, including alpha/beta/rc tags. -release = '0.9.9' - # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -# language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -# today = '' -# Else, today_fmt is used as the format for a strftime call. -# today_fmt = '%B %d, %Y' +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all documents. -# default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -# add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -# add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# show_authors = False +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -# modindex_common_prefix = [] +pygments_style = None -# -- Options for HTML output --------------------------------------------------- +# -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +# +html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. +# # html_theme_options = {} -# Add any paths that contain custom themes here, relative to this directory. -# html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -# html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -# html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -# html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -# html_favicon = None - # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -# html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -# html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# # html_sidebars = {} -# Additional templates that should be rendered to pages, maps page names to -# template names. -# html_additional_pages = {} - -# If false, no module index is generated. -# html_domain_indices = True - -# If false, no index is generated. -# html_use_index = True - -# If true, the index is split into individual pages for each letter. -# html_split_index = False - -# If true, links to the reST sources are added to the pages. -# html_show_sourcelink = True -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -# html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -# html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -# html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = None +# -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. -htmlhelp_basename = 'PySDL2Doc' - - -# -- Options for LaTeX output -------------------------------------------------- +htmlhelp_basename = 'PySDL2doc' -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -# 'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -# 'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -# 'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]). -latex_documents = [ - ('index', 'PySDL2.tex', u'PySDL2 Documentation', - u'Marcus von Appen', 'manual'), -] -# The name of an image file (relative to this directory) to place at the top of -# the title page. -# latex_logo = None +# -- Extension configuration ------------------------------------------------- -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -# latex_use_parts = False - -# If true, show page references after internal links. -# latex_show_pagerefs = False - -# If true, show URL addresses after external links. -# latex_show_urls = False - -# Documents to append as an appendix to all manuals. -# latex_appendices = [] - -# If false, no module index is generated. -# latex_domain_indices = True - - -# -- Options for manual page output -------------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'pysdl2', u'PySDL2 Documentation', - [u'Marcus von Appen'], 1) -] - -# If true, show URL addresses after external links. -# man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------------ - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', 'PySDL2', u'PySDL2 Documentation', - u'Marcus von Appen', 'PySDL2', 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -# texinfo_appendices = [] - -# If false, no module index is generated. -# texinfo_domain_indices = True +todo_include_todos = True +napoleon_google_docstring = True +autodoc_member_order = 'bysource' +graphviz_output_format = 'png' -# How to display URL addresses: 'footnote', 'no', or 'inline'. -# texinfo_show_urls = 'footnote' +# -- Options for intersphinx extension --------------------------------------- # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/': "python.inv"} +intersphinx_mapping = {'https://docs.python.org/': None} diff --git a/doc/index.rst b/doc/index.rst index f1fc3087..c11d4e56 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -4,38 +4,41 @@ PySDL2 is a wrapper around the SDL2 library and as such similar to the discontinued PySDL project. In contrast to PySDL, it has no licensing restrictions, nor does it rely on C code, but uses :mod:`ctypes` instead. -Contents -======== +Getting Started +=============== .. toctree:: :maxdepth: 2 install.rst integration.rst + faq.rst tutorial/index.rst + + +API Reference +============= + +.. toctree:: + :maxdepth: 2 + modules/index.rst - faq.rst - news.rst -Further readings: + +Project Information +=================== .. toctree:: :maxdepth: 1 + news.rst todos.rst copying.rst + Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` - - -Documentation TODOs -=================== - -.. todolist:: - -Last generated on: |today| diff --git a/doc/make.bat b/doc/make.bat deleted file mode 100644 index 7eb8a3e1..00000000 --- a/doc/make.bat +++ /dev/null @@ -1,215 +0,0 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - - -REM Check if sphinx-build is available and fallback to Python version if any -%SPHINXBUILD% 2> nul -if errorlevel 9009 goto sphinx_python -goto sphinx_ok - -:sphinx_python - -set SPHINXBUILD=python -m sphinx.__init__ -%SPHINXBUILD% 2> nul -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -:sphinx_ok - - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Priddle.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Priddle.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -:end diff --git a/doc/modules/ext/algorithms.rst b/doc/modules/ext/algorithms.rst new file mode 100644 index 00000000..47912031 --- /dev/null +++ b/doc/modules/ext/algorithms.rst @@ -0,0 +1,11 @@ +`sdl2.ext.algorithms` - Useful Algorithms +========================================= + +This module contains some useful algorithms for working with shapes and +surfaces. At present it contains functions for clipping lines to fit +within a set of 2D boundaries and for determining whether a point falls +along a given line. + +.. automodule:: sdl2.ext.algorithms + :undoc-members: + :members: diff --git a/doc/modules/ext/array.rst b/doc/modules/ext/array.rst new file mode 100644 index 00000000..5932aeda --- /dev/null +++ b/doc/modules/ext/array.rst @@ -0,0 +1,149 @@ +`sdl2.ext.array` - Tools for Working with `ctypes` Arrays +========================================================= + +This module provides a number of utilites for accessing data in ``ctypes`` +arrays and converting ``ctypes`` arrays into common Python formats. + +.. warning:: + These functions are primarily meant for internal use in PySDL2, and should + be treated as experimental. + + +Providing read-write access for sequential data +----------------------------------------------- +Two classes allow you to access sequential data in different ways. The +:class:`CTypesView` provides byte-wise access to iterable objects and allows +you to convert the object representation to matching byte-widths for +:mod:`ctypes` or other modules. + +Depending on the the underlying object and the chosen size of each particular +item of the object, the :class:`CTypesView` allows you to operate directly +on different representations of the object's contents. :: + + >>> text = bytearray("Hello, I am a simple ASCII string!") + >>> ctview = CTypesView(text, itemsize=1) + >>> ctview.view[0] = 0x61 + >>> print(text) + aello, I am a simple ASCII string!" + >>> ctview.to_uint16()[3] = 0x6554 + >>> print(text) + aello,Te am a simple ASCII string!" + +The snippet above provides a single-byte sized view on a :func:`bytearray` +object. Afterwards, the first item of the view is changed, which causes a +change on the :func:`bytearray`, on the first item as well, since both, the +:class:`CTypesView` and the :func:`bytearray` provide a byte-wise access to +the contents. + +By using :meth:`CTypesView.to_uint16()`, we change the access representation to +a 2-byte unsigned integer :mod:`ctypes` pointer and change the fourth 2-byte +value, *I* to something else. :: + + >>> text = bytearray("Hello, I am a simple ASCII string!") + >>> ctview = CTypesView(text, itemsize=2) + >>> ctview.view[0] = 0x61 + >>> print(text) + aello, I am a simple ASCII string!" + >>> ctview.to_uint16()[3] = 0x6554 + >>> print(text) aello,Te am a simple ASCII string!" + +If the encapsuled object does not provide a (writable) :func:`buffer` +interface, but is iterable, the :class:`CTypesView` will create an +internal copy of the object data using Python's :mod:`array` module and +perform all operations on that copy. :: + + >>> mylist = [18, 52, 86, 120, 154, 188, 222, 240] + >>> ctview = CTypesView(mylist, itemsize=1, docopy=True) + >>> print(ctview.object) + array('B', [18, 52, 86, 120, 154, 188, 222, 240]) + >>> ctview.view[3] = 0xFF + >>> print(mylist) + [18, 52, 86, 120, 154, 188, 222, 240] + >>> print(ctview.object) + array('B', [18, 52, 86, 255, 154, 188, 222, 240]) + +As for directly accessible objects, you can define your own itemsize to +be used. If the iterable does not provide a direct byte access to their +contents, this won't have any effect except for resizing the item +widths. :: + + >>> mylist = [18, 52, 86, 120, 154, 188, 222, 240] + >>> ctview = CTypesView(mylist, itemsize=4, docopy=True) + >>> print(ctview.object) + array('I', [18L, 52L, 86L, 120L, 154L, 188L, 222L, 240L]) + +Accessing data over multiple dimensions +--------------------------------------- +The second class, :class:`MemoryView` provides an interface to access +data over multiple dimensions. You can layout and access a simple +byte stream over e.g. two or more axes, providing a greater flexibility +for functional operations and complex data. + +Let's assume, we are reading image data from a file stream into some buffer +object and want to access and manipulate the image data. Images feature two +axes, one being the width, the other being the height, defining a rectangular +graphics area. + +When we read all data from the file, we have an one-dimensional view of the +image graphics. The :class:`MemoryView` allows us to define a +two-dimensional view over the image graphics, so that we can operate on +both, rows and columns of the image. :: + + >>> imagedata = bytearray("some 1-byte graphics data") + >>> view = MemoryView(imagedata, 1, (5, 5)) + >>> print(view) + [[s, o, m, e, ], [1, -, b, y, t], [e, , g, r, a], [p, h, i, c, s], [ , d, a, t, a]] + >>> for row in view: + ... print(row) + ... + [s, o, m, e, ] + [1, -, b, y, t] + [e, , g, r, a] + [p, h, i, c, s] + [ , d, a, t, a] + >>> for row in view: + ... row[1] = "X" + ... print row + ... + [s, X, m, e, ] + [1, X, b, y, t] + [e, X, g, r, a] + [p, X, i, c, s] + [ , X, a, t, a] + >>> print(imagedata) + sXme 1XbyteXgrapXics Xata + +On accessing a particular dimension of a :class:`MemoryView`, a new +:class:`MemoryView` is created, if it does not access a single +element. :: + + >>> firstrow = view[0] + >>> type(firstrow) + + >>> type(firstrow[0]) + + +A :class:`MemoryView` features, similar to Python's builtin +:class:`memoryview`, dimensions and strides, accessible via the +:attr:`MemoryView.ndim` and :attr:`MemoryView.strides` attributes. + + >>> view.ndim + 2 + >>> view.strides + (5, 5) + +The :attr:`MemoryView.strides`, which have to be passed on creating a +new :class:`MemoryView`, define the layout of the data over different +dimensions. In the example above, we created a 5x5 two-dimensional view +to the image graphics. :: + + >>> twobytes = MemoryView(imagedata, 2, (5, 1)) + >>> print(twobytes) + [[sX, me, 1, Xb, yt], [eX, gr, ap, Xi, cs]] + + +Array API +--------- + +.. automodule:: sdl2.ext.array + :members: diff --git a/doc/modules/ext/bitmapfont.rst b/doc/modules/ext/bitmapfont.rst new file mode 100644 index 00000000..c3e3b06b --- /dev/null +++ b/doc/modules/ext/bitmapfont.rst @@ -0,0 +1,8 @@ +`sdl2.ext.bitmapfont` - Basic Bitmap Font Rendering +=================================================== + +This module provides the :class:`~sdl2.ext.BitmapFont` class, which allows for +basic font rendering in PySDL2 without depending on the SDL_ttf library. + +.. automodule:: sdl2.ext.bitmapfont + :members: diff --git a/doc/modules/ext/color.rst b/doc/modules/ext/color.rst new file mode 100644 index 00000000..05bc09ce --- /dev/null +++ b/doc/modules/ext/color.rst @@ -0,0 +1,16 @@ +`sdl2.ext.color` - Color Representation and Conversion +====================================================== + +The `sdl2.ext.color` module provides a number of classes and functions for +working with colors. + +The primary part of this module is the `Color` class, which allows flexible +representation of colours across `sdl2.ext` functions and offers easy conversion +between colorspaces (e.g. RGB to HSV). Additionally, this module provides +functions for easily converting RGBA/ARGB integers and hexidecimal strings +to `Color` objects. + + +.. automodule:: sdl2.ext.color + :undoc-members: + :members: diff --git a/doc/modules/ext/colorpalettes.rst b/doc/modules/ext/colorpalettes.rst new file mode 100644 index 00000000..28859be1 --- /dev/null +++ b/doc/modules/ext/colorpalettes.rst @@ -0,0 +1,5 @@ +`sdl2.ext.colorpalettes` - Predefined Color Palettes +==================================================== + +.. automodule:: sdl2.ext.colorpalettes + :members: diff --git a/doc/modules/ext/common.rst b/doc/modules/ext/common.rst new file mode 100644 index 00000000..2854113e --- /dev/null +++ b/doc/modules/ext/common.rst @@ -0,0 +1,9 @@ +`sdl2.ext.common` - Frequently Used SDL2 Functions +================================================== + +This module wraps various common SDL2 functions, including methods for +initializing and quitting SDL2 and its various subsystems and for +retrieving events from the SDL2 event queue. + +.. automodule:: sdl2.ext.common + :members: diff --git a/doc/modules/ext/compat.rst b/doc/modules/ext/compat.rst new file mode 100644 index 00000000..69a4c174 --- /dev/null +++ b/doc/modules/ext/compat.rst @@ -0,0 +1,19 @@ +`sdl2.ext.compat` - Python Version Compatibility Helpers +======================================================== + +The :mod:`sdl2.ext.compat` module provides various helper functions for writing +code that works seamlessly on both Python 2.7 and Python 3.x. + +.. data:: ISPYTHON2 + + ``True``, if executed in a Python 2.x compatible interpreter, ``False`` + otherwise. + +.. data:: ISPYTHON3 + + ``True``, if executed in a Python 3.x compatible interpreter, ``False`` + otherwise. + +.. automodule:: sdl2.ext.compat + :exclude-members: long, unichr, unicode, UnsupportedError, ExperimentalWarning + :members: diff --git a/doc/modules/ext/draw.rst b/doc/modules/ext/draw.rst new file mode 100644 index 00000000..e0fa0fad --- /dev/null +++ b/doc/modules/ext/draw.rst @@ -0,0 +1,9 @@ +`sdl2.ext.draw` - Drawing Routines for Software Surfaces +======================================================== + +The `sdl2.ext.draw` module provides some basic methods for drawing to +unaccelerated 2D SDL surfaces. At present, the functions in this module can +fill the surface with a given color and draw rectangles and lines. + +.. automodule:: sdl2.ext.draw + :members: diff --git a/doc/modules/sdl2ext_ebs.rst b/doc/modules/ext/ebs.rst similarity index 100% rename from doc/modules/sdl2ext_ebs.rst rename to doc/modules/ext/ebs.rst diff --git a/doc/modules/sdl2ext_events.rst b/doc/modules/ext/events.rst similarity index 100% rename from doc/modules/sdl2ext_events.rst rename to doc/modules/ext/events.rst diff --git a/doc/modules/ext/image.rst b/doc/modules/ext/image.rst new file mode 100644 index 00000000..624bae5c --- /dev/null +++ b/doc/modules/ext/image.rst @@ -0,0 +1,22 @@ +.. currentmodule:: sdl2.ext + +`sdl2.ext.image` - Importing and Exporting Image Files +====================================================== + +The :mod:`sdl2.ext.image` module provides some simple functions for importing +images as SDL surfaces and exporting SDL surfaces to image files. + +The basic functions :func:`load_bmp` and :func:`save_bmp` load and save BMP +files, and are guaranteed to be available on all systems supported by PySDL2. +The :func:`load_img` function can be used to import additional image formats +(e.g. JPEG, PNG), but requires that the **SDL_image** library is installed on +the target system (can be installed as a Python dependency with ``pysdl2-dll`` +on platforms that support it). + +In addition to importing images from files, this module also provides the +:func:`pillow_to_surface` function for converting :obj:`Image` objects from the +`Pillow `_ Python library to SDL +surfaces. + +.. automodule:: sdl2.ext.image + :members: diff --git a/doc/modules/images/coordinate_rhs.dia b/doc/modules/ext/images/coordinate_rhs.dia similarity index 100% rename from doc/modules/images/coordinate_rhs.dia rename to doc/modules/ext/images/coordinate_rhs.dia diff --git a/doc/modules/images/coordinate_rhs.png b/doc/modules/ext/images/coordinate_rhs.png similarity index 100% rename from doc/modules/images/coordinate_rhs.png rename to doc/modules/ext/images/coordinate_rhs.png diff --git a/doc/modules/images/copprocessing.dia b/doc/modules/ext/images/copprocessing.dia similarity index 100% rename from doc/modules/images/copprocessing.dia rename to doc/modules/ext/images/copprocessing.dia diff --git a/doc/modules/images/copprocessing.png b/doc/modules/ext/images/copprocessing.png similarity index 100% rename from doc/modules/images/copprocessing.png rename to doc/modules/ext/images/copprocessing.png diff --git a/doc/modules/images/ebs.dia b/doc/modules/ext/images/ebs.dia similarity index 100% rename from doc/modules/images/ebs.dia rename to doc/modules/ext/images/ebs.dia diff --git a/doc/modules/images/ebs.png b/doc/modules/ext/images/ebs.png similarity index 100% rename from doc/modules/images/ebs.png rename to doc/modules/ext/images/ebs.png diff --git a/doc/modules/ext/images/font.png b/doc/modules/ext/images/font.png new file mode 100644 index 00000000..5eae561d Binary files /dev/null and b/doc/modules/ext/images/font.png differ diff --git a/doc/modules/images/uiprocessing.dia b/doc/modules/ext/images/uiprocessing.dia similarity index 100% rename from doc/modules/images/uiprocessing.dia rename to doc/modules/ext/images/uiprocessing.dia diff --git a/doc/modules/images/uiprocessing.png b/doc/modules/ext/images/uiprocessing.png similarity index 100% rename from doc/modules/images/uiprocessing.png rename to doc/modules/ext/images/uiprocessing.png diff --git a/doc/modules/ext/msgbox.rst b/doc/modules/ext/msgbox.rst new file mode 100644 index 00000000..afd41208 --- /dev/null +++ b/doc/modules/ext/msgbox.rst @@ -0,0 +1,9 @@ +`sdl2.ext.msgbox` - Displaying Alerts and Dialog Boxes +====================================================== + +The :mod:`sdl2.ext.msgbox` module provides a Pythonic interface for working +with the SDL MesssageBox API. These classes and functions make it easy to +create user prompts and simple alerts using the system's window manager. + +.. automodule:: sdl2.ext.msgbox + :members: diff --git a/doc/modules/sdl2ext_particles.rst b/doc/modules/ext/particles.rst similarity index 97% rename from doc/modules/sdl2ext_particles.rst rename to doc/modules/ext/particles.rst index 560bec66..2eb183c1 100644 --- a/doc/modules/sdl2ext_particles.rst +++ b/doc/modules/ext/particles.rst @@ -1,8 +1,8 @@ .. module:: sdl2.ext.particles :synopsis: A simple particle system. -sdl2.ext.particles - A simple particle system -============================================= +A simple particle system +======================== .. class:: ParticleEngine() diff --git a/doc/modules/ext/pixelaccess.rst b/doc/modules/ext/pixelaccess.rst new file mode 100644 index 00000000..cf2ee5c8 --- /dev/null +++ b/doc/modules/ext/pixelaccess.rst @@ -0,0 +1,16 @@ +.. currentmodule:: sdl2.ext + +`sdl2.ext.pixelaccess` - Array-like Access to SDL Surface Contents +================================================================== + +The :mod:`sdl2.ext.pixelaccess` module offers a number of methods for reading, +modifying, and copying the contents SDL surface objects. + +In most cases, the Numpy-based :func:`~sdl2.ext.pixels2d`, +:func:`~sdl2.ext.pixels3d`, and :func:`~sdl2.ext.surface_to_ndarray` functions +are the fastest and most flexible way of directly accessing the pixels of an +SDL surface. However, the pure-Python :class:`~sdl2.ext.PixelView` class can +be used instead to avoid adding Numpy as a dependency for your project. + +.. automodule:: sdl2.ext.pixelaccess + :members: diff --git a/doc/modules/ext/renderer.rst b/doc/modules/ext/renderer.rst new file mode 100644 index 00000000..e9d0a3db --- /dev/null +++ b/doc/modules/ext/renderer.rst @@ -0,0 +1,10 @@ +`sdl2.ext.renderer` - Accelerated 2D Rendering +============================================== + +The :mod:`sdl2.ext.renderer` module implements a Pythonic interface for working +with the SDL Renderer API, which allows for easy hardware-accelerated 2D +rendering with backends for a number of platforms (e.g. OpenGL, Direct3D, Metal) +as well as a software-accelerated fallback. + +.. automodule:: sdl2.ext.renderer + :members: diff --git a/doc/modules/ext/resources.rst b/doc/modules/ext/resources.rst new file mode 100644 index 00000000..5f7296bb --- /dev/null +++ b/doc/modules/ext/resources.rst @@ -0,0 +1,78 @@ +.. currentmodule:: sdl2.ext + +`sdl2.ext.resources` - Resource Management +========================================== + +Nearly every application or game includes resources, such as image and data +files, configuration files and so on. Accessing those files from an asset folder +hierarchy or a compressed bundle across platforms can become a complex +task. The :class:`Resources` class aims to simplify this by providing +dictionary-style access for your application's resources. + +Let's assume your application has the following installation layout: :: + + Application Directory + Application.py + Application.conf + data/ + background.jpg + button1.jpg + button2.jpg + info.dat + +Within the ``Application.py`` code, you can - completely system-agnostic - +define a new resource that keeps track of all ``data`` items. :: + + apppath = os.path.dirname(os.path.abspath(__file__)) + appresources = Resources(os.path.join(apppath, "data")) + + # Access some images + bgimage = appresources.get("background.jpg") + btn1image = appresources.get("button1.jpg") + ... + +To access individual files, you do not need to concat paths the whole +time and regardless of the current directory, your application operates +on, you can access your resource files at any time through the +:class:`Resources` instance, you created initially. + +The :class:`Resources` class is also able to scan an index archived files, +compressed via ZIP or TAR (gzip or bzip2 compression), and subdiectories +automatically. :: + + Application Directory + Application.py + Application.conf + data/ + audio/ + example.wav + background.jpg + button1.jpg + button2.jpg + graphics.zip + [tileset1.bmp + tileset2.bmp + tileset3.bmp + ] + info.dat + + tilesimage = appresources.get("tileset1.bmp") + audiofile = appresources.get("example.wav") + +If you request an indexed file via :meth:`Resources.get`, you will receive +a :class:`io.BytesIO` stream, containing the file data, for further processing. + +.. note:: + + The scanned files act as keys within the :class:`Resources` class. This + means that two files, that have the same name, but are located in different + directories, will not be indexed. Only one of them will be accessible + through the :class:`Resources` class. + + +API +--- + +.. automodule:: sdl2.ext.resources + :exclude-members: open_url + :members: diff --git a/doc/modules/sdl2ext_sprite.rst b/doc/modules/ext/spritesystem.rst similarity index 72% rename from doc/modules/sdl2ext_sprite.rst rename to doc/modules/ext/spritesystem.rst index 17ca4136..6adbf9dd 100644 --- a/doc/modules/sdl2ext_sprite.rst +++ b/doc/modules/ext/spritesystem.rst @@ -291,105 +291,3 @@ Sprite, texture and pixel surface routines Creates a :class:`Sprite` from a string of text. This method requires a :class:`sdl2.ext.FontManager` to be in *kwargs* or :attr:`default_args`. - -.. class:: Renderer(target : obj[, logical_size=None[, index=-1[, flags=sdl2.SDL_RENDERER_ACCELERATED]]) - - A rendering context for windows and sprites that can use hardware or - software-accelerated graphics drivers. - - If target is a :class:`sdl2.ext.Window` or :class:`sdl2.SDL_Window`, - *index* and *flags* are passed to the relevant - :class:`sdl2.SDL_CreateRenderer()` call. If *target* is a - :class:`SoftwareSprite` or :class:`sdl2.SDL_Surface`, the *index* - and *flags* arguments are ignored. - - .. attribute:: sdlrenderer - - The underlying :class:`sdl2.SDL_Renderer`. - - .. attribute:: rendertarget - - The target for which the :class:`Renderer` was created. - - .. attribute:: logical_size - - The logical size of the renderer. - - Setting this allows you to draw as if your renderer had this size, even - though the target may be larger or smaller. When drawing, the renderer will - automatically scale your contents to the target, creating letter-boxing or - sidebars if necessary. - - To reset your logical size back to the target's, set it to ``(0, 0)``. - - Setting this to a lower value may be useful for low-resolution effects. - - Setting this to a larger value may be useful for antialiasing. - - .. attribute:: color - - The :class:`sdl2.ext.Color` to use for draw and fill operations. - - .. attribute:: blendmode - - The blend mode used for drawing operations (fill and line). This - can be a value of - - * ``SDL_BLENDMODE_NONE`` for no blending - * ``SDL_BLENDMODE_BLEND`` for alpha blending - * ``SDL_BLENDMODE_ADD`` for additive color blending - * ``SDL_BLENDMODE_MOD`` for multiplied color blending - - .. attribute:: scale - - The horizontal and vertical drawing scale as two-value tuple. - - .. method:: clear([color=None]) - - Clears the rendering context with the currently set or passed - *color*. - - .. method:: copy(src : obj[, srcrect=None[, dstrect=None[, angle=0[, center=None[, flip=render.SDL_FLIP_NONE]]]]]) -> None - - Copies (blits) the passed *src*, which can be a :class:`TextureSprite` or - :class:`sdl2.SDL_Texture`, to the target of the - :class:`Renderer`. *srcrect* is the source rectangle to be used for - clipping portions of *src*. *dstrect* is the destination rectangle. - *angle* will cause the texture to be rotated around *center* by the given - degrees. *flip* can be one of the SDL_FLIP_* constants and will flip the - texture over its horizontal or vertical middle axis. If *src* is a - :class:`TextureSprite`, *angle*, *center* and *flip* will be set from - *src*'s attributes, if not provided. - - .. method:: draw_line(points : iterable[, color=None]) -> None - - Draws one or multiple lines on the rendering context. If *line* consists - of four values ``(x1, y1, x2, y2)`` only, a single line is drawn. If - *line* contains more than four values, a series of connected lines is - drawn. - - .. method:: draw_point(points : iterable[, color=None]) -> None - - Draws one or multiple points on the rendering context. The *points* - argument contains the x and y values of the points as simple sequence in - the form ``(point1_x, point1_y, point2_x, point2_y, ...)``. - - .. method:: draw_rect(rects : iterable[, color=None]) -> None - - Draws one or multiple rectangles on the rendering context. *rects* - contains sequences of four values denoting the x and y offset and width - and height of each individual rectangle in the form ``((x1, y1, w1, h1), - (x2, y2, w2, h2), ...)``. - - .. method:: fill(rects : iterable[, color=None]) -> None - - Fills one or multiple rectangular areas on the rendering context with - the current set or passed *color*. *rects* contains sequences of four - values denoting the x and y offset and width and height of each - individual rectangle in the form ``((x1, y1, w1, h1), (x2, y2, w2, h2), - ...)``. - - .. method:: present() -> None - - Refreshes the rendering context, causing changes to the render buffers - to be shown. diff --git a/doc/modules/ext/surface.rst b/doc/modules/ext/surface.rst new file mode 100644 index 00000000..043789e3 --- /dev/null +++ b/doc/modules/ext/surface.rst @@ -0,0 +1,10 @@ +`sdl2.ext.surface` - Creating and Manipulating Software Surfaces +================================================================ + +This module provides methods for working with :obj:`~sdl2.SDL_Surface` objects. + +Currently, the only function provided by this module is :func:`subsurface`, +which allows the creation of a new surface from a subsection of a larger one. + +.. automodule:: sdl2.ext.surface + :members: diff --git a/doc/modules/ext/ttf.rst b/doc/modules/ext/ttf.rst new file mode 100644 index 00000000..fcc5864d --- /dev/null +++ b/doc/modules/ext/ttf.rst @@ -0,0 +1,16 @@ +.. currentmodule:: sdl2.ext + +`sdl2.ext.ttf` - Rendering Text With TrueType Fonts +=================================================== + +The :mod:`sdl2.ext.ttf` module provides the :class:`~sdl2.ext.FontTTF` class, +which provides a friendly and Pythonic API for font rendering based around +the **SDL_ttf** library. **SDL_ttf** can be installed as a Python dependency +with ``pysdl2-dll`` on platforms that support it). + +Additionally, this module provides the deprecated :class:`~sdl2.ext.FontManager` +class, which provides a different (and less featureful) API for rendering text +using **SDL_ttf**. + +.. automodule:: sdl2.ext.ttf + :members: diff --git a/doc/modules/sdl2ext_gui.rst b/doc/modules/ext/uisystem.rst similarity index 98% rename from doc/modules/sdl2ext_gui.rst rename to doc/modules/ext/uisystem.rst index 02dac172..c2aa9937 100644 --- a/doc/modules/sdl2ext_gui.rst +++ b/doc/modules/ext/uisystem.rst @@ -1,7 +1,11 @@ .. currentmodule:: sdl2.ext -User interface elements -======================= +Designing component-oriented user interfaces +============================================ + +.. warning:: + This module is deprecated, and may be removed in a future release of PySDL2. + User interface elements within :mod:`sdl2.ext` are simple :class:`Sprite` objects, which are enhanced by certain input hooks; as such, they are not classes on their own, but implemented as mixins. The user input @@ -17,6 +21,7 @@ elements, which support the event type. .. _ui-elem-types: + UI element types ---------------- Every :class:`sdl2.ext` UI element is a simple :class:`Sprite` object, to diff --git a/doc/modules/ext/window.rst b/doc/modules/ext/window.rst new file mode 100644 index 00000000..dba26845 --- /dev/null +++ b/doc/modules/ext/window.rst @@ -0,0 +1,9 @@ +`sdl2.ext.window` - Creating and Working with SDL2 Windows +========================================================== + +This module contains the ``Window`` class, which provides a friendly Pythonic +API for creating, moving, resizing, closing, or otherwise working with SDL2 +windows. + +.. automodule:: sdl2.ext.window + :members: diff --git a/doc/modules/index.rst b/doc/modules/index.rst index 407011dd..7d6e637f 100644 --- a/doc/modules/index.rst +++ b/doc/modules/index.rst @@ -6,11 +6,11 @@ PySDL2 offers. If you want to have a quick overview about the modules, use the function, use the :ref:`genindex` or :ref:`search`. .. toctree:: - :maxdepth: 2 + :maxdepth: 1 sdl2.rst - sdl2_sdlgfx.rst + sdl2ext.rst + sdl2_sdlttf.rst sdl2_sdlimage.rst sdl2_sdlmixer.rst - sdl2_sdlttf.rst - sdl2ext.rst + sdl2_sdlgfx.rst diff --git a/doc/modules/sdl2.rst b/doc/modules/sdl2.rst index 73e88fcf..3ba3137e 100644 --- a/doc/modules/sdl2.rst +++ b/doc/modules/sdl2.rst @@ -115,7 +115,6 @@ available within :mod:`sdl2`. * Everything from ``SDL_atomic.h`` * Everything from ``SDL_opengl.h`` (see PyOpenGL for a compatible OpenGL API) * Everything from ``SDL_mutex.h`` -* Everything from ``SDL_sensor.h`` (currently only supports iOS & Android APIs) Additional interfaces --------------------- diff --git a/doc/modules/sdl2_sdlimage.rst b/doc/modules/sdl2_sdlimage.rst index 86c3ca9a..52ead878 100644 --- a/doc/modules/sdl2_sdlimage.rst +++ b/doc/modules/sdl2_sdlimage.rst @@ -1,13 +1,843 @@ -.. module:: sdl2.sdlimage - :synopsis: SDL2_image library wrapper - -sdl2.sdlimage - SDL2_image library wrapper -========================================== -The :mod:`sdl2.sdlimage` module is a :mod:`ctypes`-based wrapper -around the SDL2_image library. It wraps nearly all publicly accessible -structures and functions of the SDL2_image library to be accessible from -Python code. - -A detailed documentation about the behaviour of the different functions -can found within the `SDL2_image documentation -`_. +sdl2.sdlimage - Python bindings for SDL2_image +============================================== + +py-sdl2 provides bindings for SDL2_image, a library designed for use with SDL2 +that adds support for loading a wide range of different common (and uncommon) +image formats for easy use with SDL2. In addition, SDL2_image includes functions +for saving :obj:`SDL_Surface` objects to PNG and/or JPEG images. + +.. note:: + If using an alternative rendering system that doesn't use SDL surfaces as + input (e.g. PyOpenGL), the Pillow imaging library may be a better fit for + your project. + + +Initialization and library information functions +------------------------------------------------ + +.. function:: IMG_Init(flags) + + Initializes the SDL2_image library, enabling support for JPEG, PNG, TIF, + and/or WebP images as requested. All other image formats can be loaded + or used regardless of whether this has been called. + + The following are the supported init flags: + + ====== ================= + Format Init flag + ====== ================= + JPEG ``IMG_INIT_JPG`` + PNG ``IMG_INIT_PNG`` + TIFF ``IMG_INIT_TIF`` + WebP ``IMG_INIT_WEBP`` + ====== ================= + + Can be called multiple times to enable support for these formats + separately, or can initialize multiple formats at once by passing a set of + flags as a bitwise OR. You can also call this function with 0 as a flag + to check which image libraries have already been loaded. + + .. code-block:: python + + # Initialize JPEG and PNG support separately + for flag in [IMG_INIT_JPG, IMG_INIT_PNG]: + IMG_Init(flag) + err = IMG_GetError() # check for any errors loading library + if len(err): + print(err) + + # Initialize JPEG and PNG support at the same times + flags = IMG_INIT_JPG | IMG_INIT_PNG + IMG_Init(flags) + if IMG_Init(0) != flags: # verify both libraries loaded properly + print(IMG_GetError()) + + :param flags: A bitwise OR'd set of image formats to load support for. + :type flags: int + + :retuns: A bitmask of all the currently initialized image loaders. + :rtype: int + + +.. function:: IMG_Quit() + + De-initializes the SDL2_image library, disabling JPEG, PNG, TIF, and + WEBP support and freeing all associated memory. + + Once this has been called, you can re-initialize support for those + image formats using :func:`IMG_Init` and the corresponding init flags. + You only need to call this function once, no matter how many times + :func:`IMG_Init` was called. + + +.. function:: IMG_GetError() + + Returns the most recently encountered SDL2 error message, if any. + + This function is a simple wrapper around :func:`SDL_GetError`. + + :retuns: A UTF-8 encoded string describing the most recent SDL2 error. + :rtype: bytes + + +.. function:: IMG_SetError(fmt) + + Sets the most recent SDL2 error message to a given string. + + This function is a simple wrapper around :func:`SDL_SetError`. + + :param fmt: A UTF-8 encoded string containing the error message to set. + :type fmt: bytes + + :retuns: Always returns ``-1``. + :rtype: int + + +.. function:: IMG_Linked_Version() + + This function gets the version of the dynamically linked SDL2_image + library. + + :returns: A pointer to an object containing the version of the SDL2_image + library currently in use. + :rtype: POINTER(:obj:`SDL_version`) + + +Image format-checking functions +------------------------------- + +These functions are used to check whether an SDL file object +(:obj:`SDL_RWops`) is a valid image file of a given format. Note that +all of these functions will return 0 if SDL2_image was not built with +support for that format, even if it is a valid image of that type, so be +cautious when using these for formats like WEBP, JPEG, PNG, and TIFF, which +are optional when building SDL2_image. + + +.. function:: IMG_isICO(src) + + Tests whether a :obj:`SDL_RWops` file object contains a readable ICO + (Windows icon) image. + + :param src: The file object to check. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: 1 if BMPs are supported and file is a valid ICO, otherwise 0. + :rtype: int + + +.. function:: IMG_isCUR(src) + + Tests whether a :obj:`SDL_RWops` file object contains a readable CUR + (non-animated Windows cursor) image. + + :param src: The file object to check. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: 1 if BMPs are supported and file is a valid CUR, otherwise 0. + :rtype: int + + +.. function:: IMG_isBMP(src) + + Tests whether a :obj:`SDL_RWops` file object contains a readable BMP + (Windows bitmap) image. + + :param src: The file object to check. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: 1 if BMPs are supported and file is a valid BMP, otherwise 0. + :rtype: int + + +.. function:: IMG_isGIF(src) + + Tests whether a :obj:`SDL_RWops` file object contains a readable GIF + (Graphics Interchange Format) image. + + :param src: The file object to check. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: 1 if GIFs are supported and file is a valid GIF, otherwise 0. + :rtype: int + + +.. function:: IMG_isJPG(src) + + Tests whether a :obj:`SDL_RWops` file object contains a readable JPEG + image. + + :param src: The file object to check. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: 1 if JPEGs are supported and file is a valid JPEG, otherwise 0. + :rtype: int + + +.. function:: IMG_isLBM(src) + + Tests whether a :obj:`SDL_RWops` file object contains a readable LBM + (Interleaved Bitmap, ``.lbm`` or ``.iff``) image. + + :param src: The file object to check. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: 1 if LBMs are supported and file is a valid LBM, otherwise 0. + :rtype: int + + +.. function:: IMG_isPCX(src) + + Tests whether a :obj:`SDL_RWops` file object contains a readable PCX + (IBM PC Paintbrush) image. + + :param src: The file object to check. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: 1 if PCXs are supported and file is a valid PCX, otherwise 0. + :rtype: int + + +.. function:: IMG_isPNG(src) + + Tests whether a :obj:`SDL_RWops` file object contains a readable PNG + (Portable Network Graphics) image. + + :param src: The file object to check. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: 1 if PNGs are supported and file is a valid PNG, otherwise 0. + :rtype: int + + +.. function:: IMG_isPNM(src) + + Tests whether a :obj:`SDL_RWops` file object contains a readable PNM + (Portable Anymap, ``.pbm`` or ``.pgm`` or ``.ppm``) image. + + :param src: The file object to check. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: 1 if PNMs are supported and file is a valid PNM, otherwise 0. + :rtype: int + + +.. function:: IMG_isSVG(src) + + Tests whether a :obj:`SDL_RWops` file object contains a readable SVG + (Scalable Vector Graphics) image. + + :param src: The file object to check. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: 1 if SVGs are supported and file is a valid SVG, otherwise 0. + :rtype: int + + +.. function:: IMG_isTIF(src) + + Tests whether a :obj:`SDL_RWops` file object contains a readable TIFF + (Tagged Image File Format) image. + + :param src: The file object to check. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: 1 if TIFFs are supported and file is a valid TIFF, otherwise 0. + :rtype: int + + +.. function:: IMG_isXCF(src) + + Tests whether a :obj:`SDL_RWops` file object contains a readable XCF + (native GIMP format) image. + + .. note:: XCF support is currently missing in official macOS binaries + + :param src: The file object to check. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: 1 if XCFs are supported and file is a valid XCF, otherwise 0. + :rtype: int + + +.. function:: IMG_isXPM(src) + + Tests whether a :obj:`SDL_RWops` file object contains a readable XPM + (X11 Pixmap) image. + + :param src: The file object to check. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: 1 if XPMs are supported and file is a valid XPM, otherwise 0. + :rtype: int + + +.. function:: IMG_isXV(src) + + Tests whether a :obj:`SDL_RWops` file object contains a readable XV + thumbnail (XV Visual Schnauzer format) image. + + :param src: The file object to check. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: 1 if XV thumbnails are supported and file is a valid XV + thumbnail, otherwise 0. + :rtype: int + + +.. function:: IMG_isWEBP(src) + + Tests whether a :obj:`SDL_RWops` file object contains a readable WebP + image. + + :param src: The file object to check. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: 1 if WebPs are supported and file is a valid WebP, otherwise 0. + :rtype: int + + +General image loading functions +------------------------------- + +.. function:: IMG_Load(file) + + Loads an image file to a new surface. This can load all supported image + files, including TGA as long as the filename ends with ".tga". + + It is best to call this outside of event loops and keep the loaded + images around until you are really done with them, as disk speed and + image conversion to a surface can be slow. + + Once you are done with a loaded image, you can call + :func:`SDL_FreeSurface` on the returned surface pointer to free up the + memory associated with it. + + If the image format supports a transparent pixel, SDL_image will set the + colorkey for the surface. You can enable RLE acceleration on the surface + afterwards by calling:: + + SDL_SetColorKey(image, SDL_RLEACCEL, image.contents.format.colorkey) + + .. note:: If the image loader for the format of the given image requires + initialization (e.g. PNG) and it is not already initialized, + this function will attempt to load it automatically. + + :param file: A UTF8-encoded bytestring containing the path to the font + file to load. + :type file: bytes + + :returns: A pointer to the new surface containing the image, or a null + pointer if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: IMG_Load_RW(src, freesrc) + + Loads an image file from an SDL2 file object to a new surface. This can + load all supported formats, *except* TGA. See :func:`IMG_Load` for more + information. + + :param src: The file object to load an image from. + :type src: POINTER(:obj:`SDL_RWops`) + :param freesrc: If non-zero, the input file object will be closed and freed + after it has been read. + :type freesrc: int + + :returns: A pointer to the new surface containing the image, or a null + pointer if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: IMG_LoadTyped_RW(src, freesrc, type) + + Loads an image file from an SDL2 file object to a new surface, explicitly + specifying the format type of the image to load. Here are the different + possible valid format type strings: + + ============= =========================== + Format String Format Type + ============= =========================== + b"TGA" TrueVision Targa + b"CUR" Windows Cursor + b"ICO" Windows Icon + b"BMP" Windows Bitmap + b"GIF" Graphics Interchange Format + b"JPG" JPEG + b"LBM" Interleaved Bitmap + b"PCX" IBM PC Paintbrush + b"PNG" Portable Network Graphics + b"PNM" Portable Anymap + b"SVG" Scalable Vector Graphics + b"TIF" Tagged Image File Format + b"XCF" GIMP native format + b"XPM" X11 Pixmap + b"XV" XV Thumbnail + b"WEBP" WebP + ============= =========================== + + See :func:`IMG_Load` for more information. + + :param src: The file object to load an image from. + :type src: POINTER(:obj:`SDL_RWops`) + :param freesrc: If non-zero, the input file object will be closed and freed + after it has been read. + :type freesrc: int + :param type: A bytestring indicating which format to attempt to interpret + the image as. + :type type: bytes + + :returns: A pointer to the new surface containing the image, or a null + pointer if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: IMG_LoadTexture(renderer, file) + + Loads an image file to a new texture using a given renderer. This can + load all supported image files, including TGA as long as the filename + ends with ".tga". + + It is best to call this outside of event loops and keep the loaded + images around until you are really done with them, as disk speed and + image conversion to a texture can be slow. + + Once you are done with a loaded image, you can call + :func:`SDL_DestroyTexture` on the returned texture pointer to free up the + memory associated with it. + + .. note:: + If the image loader for the format of the given image requires + initialization (e.g. PNG) and it is not already initialized, this + function will attempt to load it automatically. + + :param renderer: A pointer to the SDL rendering context to create the + texture with. + :type renderer: POINTER(:obj:`SDL_Renderer`) + :param file: A UTF8-encoded bytestring containing the path to the font + file to load. + :type file: bytes) + + :returns: A pointer to the new texture containing the image, or a null + pointer if there was an error. + :rtype: POINTER(:obj:`SDL_Texture`) + + +.. function:: IMG_LoadTexture_RW(renderer, src, freesrc) + + Loads an image file from an SDL2 file object to a new texture using a + given renderer. This can load all supported formats, *except* TGA. See + :func:`IMG_LoadTexture` for more information. + + :param renderer: A pointer to the SDL rendering context to create the + texture with. + :type renderer: POINTER(:obj:`SDL_Renderer`) + :param src: The file object to load an image from. + :type src: POINTER(:obj:`SDL_RWops`) + :param freesrc: If non-zero, the input file object will be closed and freed + after it has been read. + :type freesrc: int + + :returns: A pointer to the new texture containing the image, or a null + pointer if there was an error. + :rtype: POINTER(:obj:`SDL_Texture`) + + +.. function:: IMG_LoadTextureTyped_RW(renderer, src, freesrc, type) + + Loads an image file from an SDL2 file object to a new texture, explicitly + specifying the format type of the image to load. The different possible + format type strings are listed in the documentation for + :func:`IMG_LoadTyped_RW`. + + See :func:`IMG_LoadTexture` for more information. + + :param renderer: A pointer to the SDL rendering context to create the + texture with. + :type renderer: POINTER(:obj:`SDL_Renderer`) + :param src: The file object to load an image from. + :type src: POINTER(:obj:`SDL_RWops`) + :param freesrc: If non-zero, the input file object will be closed and freed + after it has been read. + :type freesrc: int + :param type: A bytestring indicating which format to attempt to interpret + the image as. + :type type: bytes + + :returns: A pointer to the new texture containing the image, or a null + pointer if there was an error. + :rtype: POINTER(:obj:`SDL_Texture`) + + +Format-specific image loading functions +--------------------------------------- + +.. function:: IMG_LoadICO_RW(src) + + Loads an ICO (Windows icon) image from an SDL :obj:`SDL_RWops` file object. + + Use the :func:`IMG_GetError` function to check for any errors. + + :param src: The file object to load the ICO from. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: A pointer to a new surface containing the image, or ``None`` + if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: IMG_LoadCUR_RW(src) + + Loads a CUR (Windows cursor) image from an SDL :obj:`SDL_RWops` file object. + + Use the :func:`IMG_GetError` function to check for any errors. + + :param src: The file object to load the CUR from. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: A pointer to a new surface containing the image, or ``None`` + if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: IMG_LoadBMP_RW(src) + + Loads a BMP (Windows bitmap) image from an SDL :obj:`SDL_RWops` file object. + + Use the :func:`IMG_GetError` function to check for any errors. + + :param src: The file object to load the BMP from. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: A pointer to a new surface containing the image, or ``None`` + if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: IMG_LoadGIF_RW(src) + + Loads a GIF (Graphics Interchange Format) image from an SDL :obj:`SDL_RWops` + file object. + + Use the :func:`IMG_GetError` function to check for any errors. + + :param src: The file object to load the GIF from. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: A pointer to a new surface containing the image, or ``None`` + if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: IMG_LoadJPG_RW(src) + + Loads a JPEG image from an SDL :obj:`SDL_RWops` file object. + + Use the :func:`IMG_GetError` function to check for any errors. + + :param src: The file object to load the JPEG from. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: A pointer to a new surface containing the image, or ``None`` + if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: IMG_LoadLBM_RW(src) + + Loads an LBM (Interleaved Bitmap) image from an SDL :obj:`SDL_RWops` file + object. + + Use the :func:`IMG_GetError` function to check for any errors. + + :param src: The file object to load the LBM from. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: A pointer to a new surface containing the image, or ``None`` + if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: IMG_LoadPCX_RW(src) + + Loads a PCX (IBM PC Paintbrush) image from an SDL :obj:`SDL_RWops` file object. + + Use the :func:`IMG_GetError` function to check for any errors. + + :param src: The file object to load the PCX from. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: A pointer to a new surface containing the image, or ``None`` + if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: IMG_LoadPNG_RW(src) + + Loads a PNG (Portable Network Graphics) image from an SDL :obj:`SDL_RWops` + file object. + + Use the :func:`IMG_GetError` function to check for any errors. + + :param src: The file object to load the PNG from. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: A pointer to a new surface containing the image, or ``None`` + if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: IMG_LoadPNM_RW(src) + + Loads a PNM (Portable Anymap) image from an SDL :obj:`SDL_RWops` file object. + + Use the :func:`IMG_GetError` function to check for any errors. + + :param src: The file object to load the PNM from. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: A pointer to a new surface containing the image, or ``None`` + if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: IMG_LoadSVG_RW(src) + + Loads an SVG (Scalable Vector Graphics) image from an SDL :obj:`SDL_RWops` + file object. + + Use the :func:`IMG_GetError` function to check for any errors. + + :param src: The file object to load the SVG from. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: A pointer to a new surface containing the image, or ``None`` + if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: IMG_LoadTGA_RW(src) + + Loads a TGA (TrueVision Targa) image from an SDL :obj:`SDL_RWops` file + object. + + Use the :func:`IMG_GetError` function to check for any errors. + + :param src: The file object to load the TGA from. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: A pointer to a new surface containing the image, or ``None`` + if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: IMG_LoadTIF_RW(src) + + Loads a TIFF (Tagged Image File Format) image from an SDL :obj:`SDL_RWops` + file object. + + Use the :func:`IMG_GetError` function to check for any errors. + + :param src: The file object to load the TIFF from. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: A pointer to a new surface containing the image, or ``None`` + if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: IMG_LoadXCF_RW(src) + + Loads an XCF (native GIMP format) image from an SDL :obj:`SDL_RWops` file + object. + + Use the :func:`IMG_GetError` function to check for any errors. + + :param src: The file object to load the XCF from. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: A pointer to a new surface containing the image, or ``None`` + if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: IMG_LoadXPM_RW(src) + + Loads an XPM (X11 Pixmap) image from an SDL :obj:`SDL_RWops` file object. + + Use the :func:`IMG_GetError` function to check for any errors. + + :param src: The file object to load the XPM from. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: A pointer to a new surface containing the image, or ``None`` + if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: IMG_LoadXV_RW(src) + + Loads an XV thumbnail image (XV Visual Schnauzer format) from an SDL + :obj:`SDL_RWops` file object. + + Use the :func:`IMG_GetError` function to check for any errors. + + :param src: The file object to load the XV thumbnail from. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: A pointer to a new surface containing the image, or ``None`` + if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: IMG_LoadWEBP_RW(src) + + Loads a WebP image from an SDL :obj:`SDL_RWops` file object. + + Use the :func:`IMG_GetError` function to check for any errors. + + :param src: The file object to load the WebP from. + :type src: POINTER(:obj:`SDL_RWops`) + + :returns: A pointer to a new surface containing the image, or ``None`` + if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: IMG_ReadXPMFromArray(xpm) + + Loads an X11 Pixmap from an array to a new surface. The XPM format consists + of a C header with an array of strings defining the dimensions, colors, and + pixels of a pixel art image: this is the data format that this function + expects to be passed. + + .. note:: + Due to the unique input format for this function, it is not obvious how + to pass data to it using Python. If you figure out a working example, + please let us know and we'll include it here! + + :param xpm: + :type xpm: POINTER(:obj:`ctypes.c_char_p`) + + :returns: A pointer to a new surface containing the image, or ``None`` + if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +Image writing functions +----------------------- + +.. function:: IMG_SavePNG(surface, file) + + Saves an :obj:`SDL_Surface` object to a PNG file. + + .. note:: This should work regardless of whether PNG support was + successfully initialized with :func:`IMG_Init`, but the full set + of PNG features may not be available. + + :param surface: A pointer to the surface containing the image to be saved. + :type surface: POINTER(:obj:`SDL_Surface`) + :param file: A UTF-8 encoded bytestring containing the path to save the + PNG to. + :type file: bytes + + :returns: 0 on success or a negative error code on failure, can call + :func:`IMG_GetError` for more information. + :rtype: int + + +.. function:: IMG_SavePNG_RW(surface, dst, freedst) + + Saves an :obj:`SDL_Surface` object to an SDL :obj:`SDL_RWops` file object + containing a PNG file. + + See :func:`IMG_SavePNG` for more information. + + :param surface: A pointer to the surface containing the image to be saved. + :type surface: POINTER(:obj:`SDL_Surface`) + :param dst: A pointer to the file object to save the PNG to. + :type dst: POINTER(:obj:`SDL_RWops`) + :param freedst: If non-zero, the destination file object will be closed once + the PNG has been written. + :type freedst: int + + :returns: 0 on success or a negative error code on failure, can call + :func:`IMG_GetError` for more information. + :rtype: int + + +.. function:: IMG_SaveJPG(surface, file, quality) + + Saves an :obj:`SDL_Surface` object to a JPEG file at a given quality. + + JPEG support must be already initialized using :func:`IMG_Init` before this + function can be used, otherwise this function will fail without an explicit + error that can be retrieved with :func:`IMG_GetError`. + + :param surface: A pointer to the surface containing the image to be saved. + :type surface: POINTER(:obj:`SDL_Surface`) + :param file: A UTF-8 encoded bytestring containing the path to save the + JPEG to. + :type: bytes + :param quality: The quality at which to compress the JPEG, from 0 to 100 + inclusive. + :type quality: int + + :returns: 0 on success or a negative error code on failure, can call + :func:`IMG_GetError` for more information. + :rtype: int + + +.. function:: IMG_SaveJPG_RW(surface, dst, freedst, quality) + + Saves an :obj:`SDL_Surface` object to an SDL :obj:`SDL_RWops` file object + containing a JPEG file at a given quality. + + See :func:`IMG_SaveJPG` for more information. + + :param surface: A pointer to the surface containing the image to be saved. + :type surface: POINTER(:obj:`SDL_Surface`) + :param dst: A pointer to the file object to save the JPEG to. + :type dst: POINTER(:obj:`SDL_RWops`) + :param freedst: If non-zero, the destination file object will be closed once + the JPEG has been written. + :type: int + :param quality: The quality at which to compress the JPEG, from 0 to 100 + inclusive. + :type quality: int + + :returns: 0 on success or a negative error code on failure, can call + :func:`IMG_GetError` for more information. + :rtype: int + + +Module constants +---------------- + +.. data:: IMG_MAJOR_VERSION + + Latest SDL2_image library major number supported by PySDL2. + +.. data:: IMG_MINOR_VERSION + + Latest SDL2_image library minor number supported by PySDL2. + +.. data:: IMG_PATCHLEVEL + + Latest SDL2_image library patch level number supported by PySDL2. + +.. data:: IMG_INIT_JPG + + :func:`IMG_Init` flag to enable support for the JPEG image format. + +.. data:: IMG_INIT_PNG + + :func:`IMG_Init` flag to enable support for the PNG image format. + +.. data:: IMG_INIT_TIF + + :func:`IMG_Init` flag to enable support for the TIFF image format. + +.. data:: IMG_INIT_WEBP + + :func:`IMG_Init` flag to enable support for the WEBP image format. diff --git a/doc/modules/sdl2_sdlttf.rst b/doc/modules/sdl2_sdlttf.rst index 67711adf..2f538f2d 100644 --- a/doc/modules/sdl2_sdlttf.rst +++ b/doc/modules/sdl2_sdlttf.rst @@ -1,13 +1,1032 @@ -.. module:: sdl2.sdlttf - :synopsis: SDL2_ttf library wrapper - -sd2.sdlttf - SDL2_ttf library wrapper -===================================== -The :mod:`sdl2.sdlttf` module is a :mod:`ctypes`-based wrapper -around the SDL2_ttf library. It wraps nearly all publicly accessible -structures and functions of the SDL2_ttf library to be accessible from -Python code. - -A detailed documentation about the behaviour of the different functions -can found within the `SDL2_ttf documentation -`_. +sdl2.sdlttf - Python bindings for SDL2_ttf +========================================== + +py-sdl2 provides bindings for SDL2_ttf, a library designed for use with SDL2 +that provides high quality TrueType font rendering. + +The SDL2_ttf library provides functions for rendering three main formats of +text, denoted by the suffix of the function. Functions ending in ``Text`` can only +render plain ASCII text, whereas functions ending in ``UTF8`` or ``UNICODE`` can +render most unicode characters provided that a font supports them. The `UNICODE` +functions are more-or-less useless in the context of Python, since it's much +easier and more stable across platforms and versions to convert Python strings +to utf-8 bytestrings than it is to convert them to ctypes arrays of UCS-2 +characters, and the `UNICODE` functions convert their input to utf-8 before +rendering anyway so there's no functionality or character support lost by using +their `UTF8` counterparts. + +SDL2_ttf supports a range of input formats, including TrueType (.ttf) and +OpenType (.otf) fonts. It also supports different font styles, font hinting +modes, and font outlines. + +.. note:: + This module handles font sizes in units of points (pt) instead of pixels. To + obtain a font with a given pixel height, you can use the + :func:`TTF_GlyphMetrics` function to get the pixel heights of different + glyphs in the font at a given pt size and use the px/pt ratio to figure out + the pt size needed to render text at a given height in px. + + +Initialization functions +------------------------ + +.. function:: TTF_Init() + + Initializes the TTF engine. + + This must be called before using other functions in this library, except + :func:`TTF_WasInit`. SDL does not have to be initialized before this call. + + :returns: 0 if successful, -1 on error + :rtype: int + + +.. function:: TTF_Quit() + + De-initialize the TTF engine. + + Other SDL_ttf functions should not be used after calling this, except for + :func:`TTF_WasInit`, until :func:`TTF_Init` is called to re-initialize + the engine. + + +.. function:: TTF_WasInit() + + Check if the TTF engine is initialized. + + You may use this before :func:`TTF_Init` to avoid initializing twice + in a row, or to determine if you need to call :func:`TTF_Quit`. + + :returns: 1 if already initialized, 0 if not initialized. + :rtype: int + + +.. function:: TTF_OpenFont(file, ptsize) + + Opens a given font file, creating a font object from it at the specified + size. + + Point size is based on 72 DPI. File paths can be relative or absolute. + + .. code-block:: python + + fontpath = sdl2.ext.byteify(os.path.join('path', 'to', 'font.ttf')) + font = TTF_OpenFont(fontpath, 23) + + :param file: A UTF8-encoded bytestring containing the path to the font + file to load. + :type file: bytes + :param ptsize: The size in points (pt) at which to open the font. + :type ptsize: int + + :rtype: POINTER(:obj:`TTF_Font`) + + +.. function:: TTF_OpenFontIndex(file, ptsize, index) + + Opens a specific font face by index number from a given multi-face font file, + creating a font object from it at the specified size. + + Point size is based on 72 DPI. File paths can be relative or absolute. + + .. code-block:: python + + fontpath = sdl2.ext.byteify(os.path.join('path', 'to', 'font.ttf')) + font = TTF_OpenFontIndex(fontpath, 23, 2) + + :param file: A UTF8-encoded bytestring containing the path to the font + file to load. + :type file: bytes + :param ptsize: The size in points (pt) at which to open the font. + :type ptsize: int + :param index: The index of the font face to open. Must be a number from + 1 to 255. + :type index: int + + :rtype: POINTER(:obj:`TTF_Font`) + + +.. function:: TTF_OpenFontRW(src, freesrc, ptsize) + + Opens a font from an :obj:`SDL_RWops` file object, creating a font object + from it at the specified size. + + Point size is based on 72 DPI. + + .. note:: + The SDL2 RW object used to create the font (``src``) must be kept in + memory until you are done with the font. Once the ``src`` has been freed, + performing any operations with the returned :obj:`TTF_Font` will result in + a segfault. + + :param src: An ``SDL_RWops`` file object containing a valid font. + :type src: POINTER(SDL_RWops) + :param freesrc: If non-zero, the provided file object will be closed and + freed automatically when the resulting :obj:`TTF_Font` is closed (or if + an error is encountered opening the font). + :type freesrc: int + :param ptsize: The size in points (pt) at which to open the font. + :type ptsize: int + + :rtype: POINTER(:obj:`TTF_Font`) + + +.. function:: TTF_OpenFontIndexRW(src, freesrc, ptsize, index) + + Opens a specific font face by index number from an :obj:`SDL_RWops` file + object containing a multi-face font, creating a font object from it at the + specified size. + + Point size is based on 72 DPI. + + .. note:: + The SDL2 RW object used to create the font (``src``) must be kept in + memory until you are done with the font. Once the ``src`` has been freed, + performing any operations with the returned :obj:`TTF_Font` will result in + a segfault. + + :param src: An ``SDL_RWops`` file object containing a valid font. + :type src: POINTER(SDL_RWops) + :param freesrc: If non-zero, the provided file object will be closed and + freed automatically when the resulting :obj:`TTF_Font` is closed (or if + an error is encountered opening the font). + :type freesrc: int + :param ptsize: The size in points (pt) at which to open the font. + :type ptsize: int + :param index: The index of the font face to open. Must be a number from + 1 to 255. + :type index: int + + :rtype: POINTER(:obj:`TTF_Font`) + + +.. function:: TTF_CloseFont(font) + + Frees the memory used by a given font, once you are done with it. + The font cannot be used after this. + + :param font: A pointer to the font to close. + :type font: POINTER(:obj:`TTF_Font`) + + +.. function:: TTF_ByteSwappedUNICODE(swapped) + + Tells the library whether UNICODE text is generally byteswapped. + + A UNICODE BOM character in a string will override this setting for the + remainder of that string. The default mode is non-swapped, native + endianness of the CPU. + + Note that this only affects the behaviour of UNICODE (UCS-2) + functions and not UTF8 functions. + + :param swapped: If 0, native CPU endianness will be used. If not 0, + UNICODE data will be byte-swapped relative to native CPU endianness. + :type swapped: int + + +.. function:: TTF_Linked_Version() + + This function gets the version of the dynamically linked SDL2_ttf library. + + :returns: a pointer to an object containing the version of the SDL2_ttf + library currently in use. + :rtype: POINTER(:obj:`SDL_version`) + + + +Font attribute functions +------------------------ + +.. function:: TTF_SetFontStyle(font, style) + + Sets the style for a given font, as specified using the following + constants: + + ============= =========================== + Style Constant + ============= =========================== + Normal ``TTF_STYLE_NORMAL`` + Bold ``TTF_STYLE_BOLD`` + Italics ``TTF_STYLE_ITALICS`` + Underlined ``TTF_STYLE_UNDERLINE`` + Strikethrough ``TTF_STYLE_STRIKETHROUGH`` + ============= =========================== + + Multiple font styles (e.g. bold and italics) can be combined using the + bitwise ``|`` operator. + + .. code-block:: python + + underlined_bold = (TTF_STYLE_BOLD | TTF_STYLE_UNDERLINE) + TTF_SetFontStyle(font, underlined_bold) + + .. note:: + Setting the underline style for a font may cause the surfaces created by + :obj:`TTF_RenderGlyph` functions to be taller, in order to make room for + the underline to be drawn underneath. + + :param font: The loaded font for which the style should be set. + :type font: POINTER(:obj:`TTF_Font`) + :param style: A bitmask specifying the new style to use for the font. + :type style: int + + +.. function:: TTF_GetFontStyle(font) + + Retrieves the rendering style of a given font. Returns one of the constants + specified in the documentation for :func:`TTF_SetFontStyle`. + + :param font: The loaded font to get the current style of. + :type font: POINTER(:obj:`TTF_Font`) + + :rtype: int + + +.. function:: TTF_SetFontOutline(font, outline) + + Sets the outline thickness (in pixels) for a given font. + + If the outline is set to zero, outlining will be disabled for the font. + + :param font: The loaded font to set the outline thickness for. + :type font: POINTER(:obj:`TTF_Font`) + :param outline: The new outline thickness to use for the font. + :type outline: int + + +.. function:: TTF_GetFontOutline(font) + + Retrieves the outline thickness (in pixels) of a given font. + + :param font: The loaded font to get the current outline thickness of. + :type font: POINTER(:obj:`TTF_Font`) + + :rtype: int + + +.. function:: TTF_SetFontHinting(font, hinting) + + Sets the hinting mode for a given font, as specified using one of the + following constants: + + ============= ======================= + Hinting type Constant + ============= ======================= + Normal ``TTF_HINTING_NORMAL`` + Light ``TTF_HINTING_LIGHT`` + Mono ``TTF_HINTING_MONO`` + None ``TTF_HINTING_NONE`` + ============= ======================= + + If no hinting mode is is explicitly set, "normal" hinting is used for + rendering. + + :param font: The loaded font to set the hinting mode setting for. + :type font: POINTER(:obj:`TTF_Font`) + :param hinting: A constant specifying the type of hinting to use when + rendering the font. + :type hinting: int + + +.. function:: TTF_GetFontHinting(font) + + Retrieves the current hinting setting of a given font. Returns one of the + constants specified in the documentation for :func:`TTF_SetFontHinting`. + + :param font: The loaded font to get the current hinting mode of. + :type font: POINTER(:obj:`TTF_Font`) + + :rtype: int + + +.. function:: TTF_FontHeight(font) + + Get the maximum pixel height of all glyphs of a given font. This is + usually equal to point size. + + You can use this height for rendering text as close together vertically + as possible, though adding at least one pixel height to it will space it + so they can't touch. + + :param font: The loaded font to get the maximum height of. + :type font: POINTER(:obj:`TTF_Font`) + + :returns: The maximum pixel height of all glyphs in the font. + :rtype: int + + +.. function:: TTF_FontAscent(font) + + Get the maximum pixel ascent of all glyphs of a given font. This can + also be interpreted as the distance from the top of the font to the + baseline. + + :param font: The loaded font to get the ascent (height above baseline) of. + :type font: POINTER(:obj:`TTF_Font`) + + :returns: A positive value in pixels, relative to the baseline. + :rtype: int + + +.. function:: TTF_FontDescent(font) + + Get the maximum pixel descent of all glyphs of a given font. This can + also be interpreted as the distance from the baseline to the bottom of + the font. + + :param font: The loaded font to get the descent (height below baseline) of. + :type font: POINTER(:obj:`TTF_Font`) + + :returns: A negative value in pixels, relative to the baseline. + :rtype: int + + +.. function:: TTF_FontLineSkip(font) + + Gets the recommended spacing between lines of text for a given font. + This is usually larger than the result of :func:`TTF_FontHeight`. + + :param font: The loaded font to get the suggested line skip height for. + :type font: POINTER(:obj:`TTF_Font`) + + :returns: The recommended height (in pixels) for each line of text. + :rtype: int + + +.. function:: TTF_GetFontKerning(font) + + Gets whether or not kerning is enabled for a given font. + + :param font: The loaded font to get the kerning status of. + :type font: POINTER(:obj:`TTF_Font`) + + :returns: 0 if kerning disabled, non-zero if kerning enabled. + :rtype: int + + +.. function:: TTF_SetFontKerning(font, allowed) + + Enables or disables font kerning for a given font. Kerning is enabled + for all fonts by default. + + :param font: The loaded font to enable or disable kerning for. + :type font: POINTER(:obj:`TTF_Font`) + :param allowed: 0 to disable kerning, non-zero to allow it. + :type allowed: int + + +.. function:: TTF_FontFaces(font) + + Get the number of faces ("sub-fonts") available in a given font. + + This is a count of the number of specific fonts (based on size and style + and other typographical features) contained in the font itself. + + :param font: The loaded font to get the number of available faces from. + :type font: POINTER(:obj:`TTF_Font`) + + :returns: The number of faces in the font. + :rtype: int + + +.. function:: TTF_FontFaceIsFixedWidth(font) + + Test if the current font face of a given font is fixed width. + + Fixed width fonts are monospace, meaning every character that exists in the + font is the same width, thus you can assume that a rendered string's width + is going to be the result of a simple calculation: + ``glyph_width * string_length``. + + :param font: The loaded font to get the fixed width status of. + :type font: POINTER(:obj:`TTF_Font`) + + :returns: An int greater than 0 if the font is fixed width, otherwise 0. + :rtype: int + + +.. function:: TTF_FontFaceFamilyName(font) + + Gets the current font face family name from a given font. + + :param font: The loaded font to get the current face family name of. + :type font: POINTER(:obj:`TTF_Font`) + + :returns: The name of the current family of the given font, or ``None`` + if not available. + :rtype: bytes + + +.. function:: TTF_FontFaceStyleName(font) + + Gets the current font face style name from a given font. + + :param font: The loaded font to get the current face style of. + :type font: POINTER(:obj:`TTF_Font`) + + :returns: The name of the current style name of the given font, or ``None`` + if not available. + :rtype: bytes + + +.. function:: TTF_GlyphIsProvided(font, ch) + + Checks whether a glyph is provided by a given font. + + :param font: The loaded font to get the glyph availability in. + :type font: POINTER(:obj:`TTF_Font`) + :param ch: The UNICODE (UCS-2) integer of the glyph to test availability of. + :type ch: int + + :rtype: int + + +.. function:: TTF_GlyphMetrics(font, ch, minx, maxx, miny, maxy, advance) + + Gets the metrics (dimensions) of a glyph for a given font. + + .. code-block:: python + + from ctypes import c_int, byref + + minX, maxX, minY, maxY = c_int(0), c_int(0), c_int(0), c_int(0) + adv = c_int(0) + TTF_GlyphMetrics( + font, ord(char), + byref(minX), byref(maxX), byref(minY), byref(maxY), byref(adv) + ) + results = [x.value for x in (minX, maxX, minY, maxY, adv)] + + To understand what these metrics mean, here is a useful link: + http://freetype.sourceforge.net/freetype2/docs/tutorial/step2.html + + :param font: The loaded font from which to get the glyph metrics of ch. + :type font: POINTER(:obj:`TTF_Font`) + :param ch: The UNICODE (UCS-2) integer of the glyph to get metrics for. + :type ch: int + :param minx: Integer pointer in which to store the glyph's minimum X offset. + :type minx: POINTER(:obj:`ctypes.c_int`) + :param maxx: Integer pointer in which to store the glyph's maximum X offset. + :type maxx: POINTER(:obj:`ctypes.c_int`) + :param miny: Integer pointer in which to store the glyph's minimum Y offset. + :type miny: POINTER(:obj:`ctypes.c_int`) + :param maxy: Integer pointer in which to store the glyph's maximum Y offset. + :type maxy: POINTER(:obj:`ctypes.c_int`) + :param advance: Integer pointer in which to store the glyph's advance offset. + :type advance: POINTER(:obj:`ctypes.c_int`) + + :returns: 0 on success, with all parameters set to the glyph metric as + appropriate. -1 on errors, e.g. when the named glyph does not exist in + the font. + :rtype: int + + +.. function:: TTF_GetFontKerningSizeGlyphs(font, previous_ch, ch) + + Gets the kerning size of two glyphs (by FreeType index) for a given font. + + .. note:: + The units of the return type of this function are undocumented in + SDL_ttf. If you figure out what they are, please let us know! + + :param font: A pointer to the font to get the kerning size for. + :type font: POINTER(:obj:`TTF_Font`) + :param previous_ch: The UNICODE (UCS-2) integer of the first glyph. + :type previous_ch: int + :param ch: The UNICODE (UCS-2) integer of the second glyph. + :type ch: int + + + +Font rendering functions +------------------------ + +.. function:: TTF_SizeText(font, text, w, h) + + Calculates the resulting surface size of an ASCII-encoded string rendered + using a given font. No actual rendering is done, but correct kerning is + performed to get the actual width. The height returned in ``h`` is the same + as that returned by :func:`TTF_FontHeight`. + + :param font: The loaded font to use for surface size calculations. + :type font: POINTER(:obj:`TTF_Font`) + :param text: The ASCII string to determine the surface size of. + :type text: bytes + :param w: Integer pointer in which to store the surface width in pixels. + :type w: POINTER(:obj:`ctypes.c_int`) + :param h: Integer pointer in which to store the surface height in pixels. + :type h: POINTER(:obj:`ctypes.c_int`) + + :returns: 0 if successful, -1 on error (e.g. if a glyph is not found in + the provided font) + :rtype: int + + +.. function:: TTF_SizeUTF8(font, text, w, h) + + Calculates the resulting surface size of a UTF-8 encoded string rendered + using a given font. See :func:`TTF_SizeText` for more info. + + :param font: The loaded font to use for surface size calculations. + :type font: POINTER(:obj:`TTF_Font`) + :param text: The UTF-8 encoded string to determine the surface size of. + :type text: bytes + :param w: Integer pointer in which to store the surface width in pixels. + :type w: POINTER(:obj:`ctypes.c_int`) + :param h: Integer pointer in which to store the surface height in pixels. + :type h: POINTER(:obj:`ctypes.c_int`) + + :returns: 0 if successful, -1 on error (e.g. if a glyph is not found in + the provided font) + :rtype: int + + +.. function:: TTF_SizeUNICODE(font, text, w, h) + + Calculates the resulting surface size of a UCS-2 encoded string rendered + using a given font. + + See :func:`TTF_SizeText` and :func:`TTF_RenderUNICODE_Solid` for more info. + + :param font: The loaded font to use for surface size calculations. + :type font: POINTER(:obj:`TTF_Font`) + :param text: A ctypes uint16 array containing the glyphs of the UCS-2 string + for which the rendered surface size should be calculated. + :type text: POINTER(Uint16) + :param w: Integer pointer in which to store the surface width in pixels. + :type w: POINTER(:obj:`ctypes.c_int`) + :param h: Integer pointer in which to store the surface height in pixels. + :type h: POINTER(:obj:`ctypes.c_int`) + + :returns: 0 if successful, -1 on error (e.g. if a glyph is not found in + the provided font) + :rtype: int + + +.. function:: TTF_RenderText_Solid(font, text, fg) + + Renders a string of ASCII encoded text to a new 8-bit palettized surface + with a transparent background and no antialiasing, using the given font + and color. + + The 0 pixel is the colorkey, giving a transparent background, and the 1 + pixel is set to the text color. This allows you to change the color without + having to render the text again. Palette index 0 is not drawn when the + returned surface is blitted to another surface, since it is the colorkey + and thus transparent, though its actual color is 255 minus each of the RGB + components of the foreground color. + + This is the fastest of all the text rendering types. The resulting surface + has a transparent background unlike :func:`TTF_RenderText_Shaded`, but the + rendered text is not antialised and will thus appear pixelated and difficult + to read at small sizes. The resulting surface should blit faster than the + one returned by :func:`TTF_RenderText_Blended`. This rendering type should + be used in cases when you need to render lots of text very quickly (e.g. if + you're updating it every frame) or when you don't care about antialiasing. + + :param font: The loaded font to render the text with. + :type font: POINTER(:obj:`TTF_Font`) + :param text: The ASCII string to render. + :type text: bytes + :param fg: The color to render the text in. This becomes colormap index 1. + :type fg: :obj:`SDL_Color` + + :returns: A pointer to the new surface containing the rendered text, or + ``None`` if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: TTF_RenderUTF8_Solid(font, text, fg) + + Renders a string of UTF-8 encoded text to a new 8-bit palettized surface + with a transparent background and no antialiasing, using the given font + and color. See :func:`TTF_RenderText_Solid` for more info. + + :param font: The loaded font to render the text with. + :type font: POINTER(:obj:`TTF_Font`) + :param text: The UTF-8 string to render. + :type text: bytes + :param fg: The color to render the text in. This becomes colormap index 1. + :type fg: :obj:`SDL_Color` + + :returns: A pointer to the new surface containing the rendered text, or + ``None`` if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: TTF_RenderUNICODE_Solid(font, text, fg) + + Renders a string of UCS-2 encoded text to a new 8-bit palettized surface + with a transparent background and no antialiasing, using the given font + and color. See :func:`TTF_RenderText_Solid` for more info. + + The required text input format for this function is a ctypes array of + UNICODE (UCS-2) glyphs in uint16 format, optionally terminated by a + byte-order mark (``UNICODE_BOM_NATIVE`` or ``UNICODE_BOM_SWAPPED``) + indicating how the text should be interpreted. Python strings can be + converted to this format using the following code: + + .. code-block:: python + + # Generate UCS-2 array from Python string + teststr = u"Hello world!" + strlen = len(teststr + 1) # +1 for byte-order mark + intstr = unpack('H' * strlen, teststr.encode('utf-16')) + strarr = (ctypes.c_uint16 * strlen)(*intstr) + + # Render UCS-2 string + col = SDL_Color(0, 0, 0) + rendered = TTF_RenderUNICODE_Solid(font, strarr, col) + + Unless there is a very specific need, the ``TTF_RenderUTF8`` functions should + always be used instead of their ``TTF_RenderUNICODE`` counterparts. In + addition to having a much friendlier Python API and being more stable (see + below), SDL_ttf uses the ``TTF_RenderUTF8`` functions internally for all the + ``TTF_RenderUNICODE`` functions anyway so there is no benefit in terms of + supporting a wider range of characters. + + .. note:: + The exact surface size generated by this and other UNICODE rendering + functions varies seemingly at random on certain platforms and Python + versions, even when provided the exact same array of integers as input. + The Text and UTF8 rendering functions do not share this instability. + + :param font: The loaded font to render the text with. + :type font: POINTER(:obj:`TTF_Font`) + :param text: A ctypes uint16 array containing the glyphs of the UCS-2 string + to render. + :type text: POINTER(Uint16) + :param fg: The color to render the text in. This becomes colormap index 1. + :type fg: :obj:`SDL_Color` + + :returns: A pointer to the new surface containing the rendered text, or + ``None`` if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: TTF_RenderGlyph_Solid(font, ch, fg) + + Renders a single UNICODE (UCS-2) glyph to a new 8-bit palettized surface + with a transparent background and no antialiasing, using the given font + and color. + + The 0 pixel is the colorkey, giving a transparent background, and the 1 + pixel is set to the text color. The glyph is rendered without any padding + or centering in the X direction, and is aligned normally in the Y direction. + + :param font: The loaded font to render the glyph with. + :type font: POINTER(:obj:`TTF_Font`) + :param ch: The UNICODE (UCS-2) integer of the glyph to render. + :type ch: int + :param fg: The color to render the glyph in. This becomes colormap index 1. + :type fg: :obj:`SDL_Color` + + :returns: A pointer to the new surface containing the rendered text, or + ``None`` if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: TTF_RenderText_Shaded(font, text, fg, bg) + + Renders a string of ASCII encoded text to a new 8-bit palettized surface + with a solid background and antialiasing, using the given font and colors. + + The 0 pixel is background, while other pixels have varying degrees of the + foreground color. This results in a box of the background color around the + text in the foreground color. + + This is the second-fastest of the text rendering types, being slightly + faster than :func:`TTF_RenderText_Blended` but slower than + :func:`TTF_RenderText_Solid`. The rendered text will be antialiased, but the resulting + surface will have a solid background colour instead of a transparent one. + Surfaces rendered with this function should blit as quickly as those created + with :func:`TTF_RenderText_Blended`. This rendering type should + be used in cases when you want nice-looking text but don't need background + transparency. + + :param font: The loaded font to render the text with. + :type font: POINTER(:obj:`TTF_Font`) + :param text: The ASCII string to render. + :type text: bytes + :param fg: The color to render the text in. This becomes colormap index 1. + :type fg: :obj:`SDL_Color` + :param bg: The color to fill the background with. This becomes colormap + index 0. + :type bg: :obj:`SDL_Color` + + :returns: A pointer to the new surface containing the rendered text, or + ``None`` if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: TTF_RenderUTF8_Shaded(font, text, fg, bg) + + Renders a string of UTF-8 encoded text to a new 8-bit palettized surface + with a solid background and antialiasing, using the given font and color. + See :func:`TTF_RenderText_Shaded` for more info. + + :param font: The loaded font to render the text with. + :type font: POINTER(:obj:`TTF_Font`) + :param text: The UTF-8 string to render. + :type text: bytes + :param fg: The color to render the text in. This becomes colormap index 1. + :type fg: :obj:`SDL_Color` + :param bg: The color to fill the background with. This becomes colormap + index 0. + :type bg: :obj:`SDL_Color` + + :returns: A pointer to the new surface containing the rendered text, or + ``None`` if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: TTF_RenderUNICODE_Shaded(font, text, fg, bg) + + Renders a string of UCS-2 encoded text to a new 8-bit palettized surface + with a solid background and antialiasing, using the given font and color. + See :func:`TTF_RenderText_Shaded` for more info. + + The expected input format for this function, along with its pitfalls, is + described in the documentation for :func:`TTF_RenderUNICODE_Solid`. + + :param font: The loaded font to render the text with. + :type font: POINTER(:obj:`TTF_Font`) + :param text: A ctypes uint16 array containing the glyphs of the UCS-2 string + to render. + :type text: POINTER(Uint16) + :param fg: The color to render the text in. This becomes colormap index 1. + :type fg: :obj:`SDL_Color` + :param bg: The color to fill the background with. This becomes colormap + index 0. + :type bg: :obj:`SDL_Color` + + :returns: A pointer to the new surface containing the rendered text, or + ``None`` if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: TTF_RenderGlyph_Shaded(font, ch, fg, bg) + + Renders a single UNICODE (UCS-2) glyph to a new 8-bit palettized surface + with a solid background and antialiasing, using the given font and colors. + + The 0 pixel is the colorkey, giving a transparent background, and the 1 + pixel is set to the text color. The glyph is rendered without any padding + or centering in the X direction, and is aligned normally in the Y direction. + + :param font: The loaded font to render the glyph with. + :type font: POINTER(:obj:`TTF_Font`) + :param ch: The UNICODE (UCS-2) integer of the glyph to render. + :type ch: int + :param fg: The color to render the glyph in. This becomes colormap index 1. + :type fg: :obj:`SDL_Color` + :param bg: The color to fill the background with. This becomes colormap + index 0. + :type bg: :obj:`SDL_Color` + + :returns: A pointer to the new surface containing the rendered text, or + ``None`` if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: TTF_RenderText_Blended(font, text, fg) + + Renders a string of ASCII encoded text to a new 32-bit ARGB surface with + a transparent background and antialiasing, using the given font and color. + + This is the slowest (but best looking) of the text rendering types. The + rendered text will be antialiased on a transparent surface using alpha + blending. Surfaces rendered with this function will blit slower than those + rendered with :func:`TTF_RenderText_Solid` or :func:`TTF_RenderText_Shaded`. + This rendering type should be used in cases when you want to overlay + rendered text over something else, and in in most other cases where high + performance isn't a major concern. + + .. note:: To render an RGBA surface instead of an ARGB one, just swap the + R and B values when creating the SDL_Color. + + :param font: The loaded font to render the text with. + :type font: POINTER(:obj:`TTF_Font`) + :param text: The ASCII string to render. + :type text: bytes + :param fg: The color to render the text in. + :type fg: :obj:`SDL_Color` + + :returns: A pointer to the new surface containing the rendered text, or + ``None`` if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: TTF_RenderUTF8_Blended(font, text, fg) + + Renders a string of UTF-8 encoded text to a new 32-bit ARGB surface with + a transparent background and antialiasing, using the given font and color. + See :func:`TTF_RenderText_Blended` for more info. + + :param font: The loaded font to render the text with. + :type font: POINTER(:obj:`TTF_Font`) + :param text: The UTF-8 string to render. + :type text: bytes + :param fg: The color to render the text in. + :type fg: :obj:`SDL_Color` + + :returns: A pointer to the new surface containing the rendered text, or + ``None`` if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: TTF_RenderUNICODE_Blended(font, text, fg) + + Renders a string of UCS-2 encoded text to a new 32-bit ARGB surface with + a transparent background and antialiasing, using the given font and color. + See :func:`TTF_RenderText_Blended` for more info. + + The expected input format for this function, along with its pitfalls, is + described in the documentation for :func:`TTF_RenderUNICODE_Solid`. + + :param font: The loaded font to render the text with. + :type font: POINTER(:obj:`TTF_Font`) + :param text: A ctypes uint16 array containing the glyphs of the UCS-2 string + to render. + :type text: POINTER(Uint16) + :param fg: The color to render the text in. + :type fg: :obj:`SDL_Color` + + :returns: A pointer to the new surface containing the rendered text, or + ``None`` if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: TTF_RenderGlyph_Blended(font, ch, fg) + + Renders a single UNICODE (UCS-2) glyph to a new 32-bit ARGB surface with a + transparent background and antialiasing, using the given font and color. + + The rendered glyph will be antialiased on a transparent surface using alpha + blending. The glyph is rendered without any padding or centering in the X + direction, and is aligned normally in the Y direction. + + :param font: The loaded font to render the glyph with. + :type font: POINTER(:obj:`TTF_Font`) + :param ch: The UNICODE (UCS-2) integer of the glyph to render. + :type ch: int + :param fg: The color to render the glyph in. + :type fg: :obj:`SDL_Color` + + :returns: A pointer to the new surface containing the rendered text, or + ``None`` if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: TTF_RenderText_Blended_Wrapped(font, text, fg, wrapLength) + + Renders a string of ASCII encoded text to a new 32-bit ARGB surface with + a transparent background and antialiasing, using the given font and color. + Text is wrapped to multiple lines on line endings and on word boundaries + if it extends beyond wrapLength in pixels. + + See :func:`TTF_RenderText_Blended` for more info. + + :param font: The loaded font to render the text with. + :type font: POINTER(:obj:`TTF_Font`) + :param text: The ASCII string to render. + :type text: bytes + :param fg: The color to render the text in. + :type fg: :obj:`SDL_Color` + :param wrapLength: The maximum width of the text in pixels. + :type wrapLength: int + + :returns: A pointer to the new surface containing the rendered text, or + ``None`` if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: TTF_RenderUTF8_Blended_Wrapped(font, text, fg, wrapLength) + + Renders a string of UTF-8 encoded text to a new 32-bit ARGB surface with + a transparent background and antialiasing, using the given font and color. + Text is wrapped to multiple lines on line endings and on word boundaries + if it extends beyond wrapLength in pixels. + + See :func:`TTF_RenderText_Blended` for more info. + + :param font: The loaded font to render the text with. + :type font: POINTER(:obj:`TTF_Font`) + :param text: The UTF-8 string to render. + :type text: bytes + :param fg: The color to render the text in. + :type fg: :obj:`SDL_Color` + :param wrapLength: The maximum width of the text in pixels. + :type wrapLength: int + + :returns: A pointer to the new surface containing the rendered text, or + ``None`` if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + +.. function:: TTF_RenderUNICODE_Blended_Wrapped(font, text, fg, wrapLength) + + Renders a string of UCS-2 encoded text to a new 32-bit ARGB surface with + a transparent background and antialiasing, using the given font and color. + Text is wrapped to multiple lines on line endings and on word boundaries + if it extends beyond wrapLength in pixels. + + See :func:`TTF_RenderText_Blended` and :func:`TTF_RenderUNICODE_Solid` for + more info. + + :param font: The loaded font to render the text with. + :type font: POINTER(:obj:`TTF_Font`) + :param text: A ctypes uint16 array containing the glyphs of the UCS-2 string + to render. + :type text: POINTER(Uint16) + :param fg: The color to render the text in. + :type fg: :obj:`SDL_Color` + :param wrapLength: The maximum width of the text in pixels. + :type wrapLength: int + + :returns: A pointer to the new surface containing the rendered text, or + ``None`` if there was an error. + :rtype: POINTER(:obj:`SDL_Surface`) + + + +Module constants +---------------- + +.. data:: TTF_MAJOR_VERSION + + Latest SDL2_ttf library major number supported by PySDL2. + +.. data:: TTF_MINOR_VERSION + + Latest SDL2_ttf library minor number supported by PySDL2. + +.. data:: TTF_PATCHLEVEL + + Latest SDL2_ttf library patch level number supported by PySDL2. + +.. data:: UNICODE_BOM_NATIVE + + This allows you to switch byte-order of UNICODE (UCS-2) text data to + native order, meaning the mode of your CPU. This is meant to be used + in UNICODE strings that you are using with the SDL2_ttf API. Not needed + for UTF8 strings. + +.. data:: UNICODE_BOM_SWAPPED + + This allows you to switch byte-order of UNICODE (UCS-2) text data to + swapped order, meaning the reversed mode of your CPU. Thus, if your CPU + is LSB, then the data will be interpretted as MSB. This is meant to be + used in UNICODE strings that you are using with the SDL2_ttf API. Not + needed for UTF8 strings. + +.. data:: TTF_STYLE_NORMAL + + Used to indicate regular, normal, plain rendering style. + +.. data:: TTF_STYLE_BOLD + + Used to indicate bold rendering style. This is used in a bitmask along + with other styles. + +.. data:: TTF_STYLE_ITALIC + + Used to indicate italicized rendering style. This is used in a bitmask + along with other styles. + +.. data:: TTF_STYLE_UNDERLINE + + Used to indicate underlined rendering style. This is used in a bitmask + along with other styles. + +.. data:: TTF_STYLE_STRIKETHROUGH + + Used to indicate strikethrough rendering style. This is used in a bitmask + along with other styles. + +.. data:: TTF_HINTING_NORMAL + + Used to indicate set hinting type to normal. + This corresponds to the default hinting algorithm, optimized for standard + gray-level rendering. + +.. data:: TTF_HINTING_LIGHT + + Used to indicate set hinting type to light. + A lighter hinting algorithm for non-monochrome modes. Many generated + glyphs are more fuzzy but better resemble its original shape. A bit like + rendering on macOS. + +.. data:: TTF_HINTING_MONO + + Used to indicate set hinting type to monochrome. + Strong hinting algorithm that should only be used for monochrome output. + The result is probably unpleasant if the glyph is rendered in + non-monochrome modes. + +.. data:: TTF_HINTING_NONE + + Used to indicate set hinting type to none. + No hinting is used, so the font may become very blurry or messy at + smaller sizes. diff --git a/doc/modules/sdl2ext.rst b/doc/modules/sdl2ext.rst index f42c897c..629faf78 100644 --- a/doc/modules/sdl2ext.rst +++ b/doc/modules/sdl2ext.rst @@ -1,33 +1,111 @@ .. module:: sdl2.ext - :synopsis: Python extensions for SDL2 + :synopsis: Helpful wrappers for the SDL2 API -sdl2.ext - Python extensions for SDL2 -===================================== -The :mod:`sdl2.ext` package provides advanced functionality for creating -applications using SDL2 and Python. It offers a rich set of modules, classes -and functions, such as easy image loading, basic user interface elements, -resource management and sprite and (on-screen) scene systems. +sdl2.ext - Helpful wrappers for the SDL2 API +============================================ -Learn more about +The :mod:`sdl2.ext` module provides a rich set of modules, classes, and +functions for creating games and other applications using PySDL2. + +The aim of the `sdl2.ext` module is to wrap commonly-used parts of the SDL2 API +in a more friendly and Pythonic manner, reducing the need for developers to +understand the intricacies of working with ``ctypes`` and making it simpler and +more fun to get PySDL2 programs up and running. + +In addition, this module provides a number of template classes and utility +functions for working with colors, input events, file assets, and more. + +Due to its broad scope, the :mod:`sdl2.ext` module is divided into a number of +submodules. However, everything in these submodules can be imported directly +from :mod:`sdl2.ext`. For example, ``from sdl2.ext import Window`` is exactly +the same as ``from sdl2.ext.window import Window``. + + +SDL2 module wrappers +-------------------- + +Some parts of the :mod:`sdl2.ext` module contain Pythonic wrappers for common +and/or unpleasant parts of the SDL2 API. At present, these modules include: + +.. toctree:: + :maxdepth: 1 + + ext/common.rst + ext/window.rst + ext/renderer.rst + ext/msgbox.rst + + +Utilities for working with SDL2 in Python +----------------------------------------- + +Other parts of this module implement functions and classes that make it easier +to work with SDL2 functions and data structures in Python (and vice versa). +These include functions for converting text to and from UTF8-encoded bytes, +reading and writing ``ctypes`` arrays, and casting :obj:`~sdl2.SDL_Surface` +objects to Numpy arrays: + +.. toctree:: + :maxdepth: 1 + + ext/compat.rst + ext/array.rst + ext/pixelaccess.rst + + +SDL2-based extensions +--------------------- + +In addition to simple Pythonic wrappers for SDL2 functions and structures, the +:mod:`sdl2.ext` module also offers a number of high-level classes and functions +that use SDL2 internally to provide APIs for font rendering, building GUIs, +importing images, and more: + +.. toctree:: + :maxdepth: 1 + + ext/surface.rst + ext/draw.rst + ext/image.rst + ext/ttf.rst + ext/bitmapfont.rst + + +Useful helpers for building apps +-------------------------------- + +Beyond wrappers and utilities for working with SDL2's API, the +:mod:`sdl2.ext` module also includes a number of submodules with various +functions and classes to help facilitate general game and app development with +PySDL2. These include classes for managing program resource files, working +with colors, and more: .. toctree:: - :maxdepth: 1 - - sdl2ext_algorithms.rst - sdl2ext_array.rst - sdl2ext_color.rst - sdl2ext_colorpalettes.rst - sdl2ext_common.rst - sdl2ext_compat.rst - sdl2ext_draw.rst - sdl2ext_ebs.rst - sdl2ext_events.rst - sdl2ext_font.rst - sdl2ext_gui.rst - sdl2ext_image.rst - sdl2ext_particles.rst - sdl2ext_pixelaccess.rst - sdl2ext_resources.rst - sdl2ext_sprite.rst - sdl2ext_surface.rst - sdl2ext_window.rst + :maxdepth: 1 + + ext/resources.rst + ext/color.rst + ext/colorpalettes.rst + ext/algorithms.rst + + +Component-Oriented Systems +-------------------------- + +PySDL2 contains several modules designed around the concept of +component-oriented game programming. These modules define various types of +"systems" for processing events and "factories" for creating different types +of objects, among other things. + +.. note:: + These modules are not regularly maintained, and their documentation + may be inaccurate or out-of-date. + +.. toctree:: + :maxdepth: 1 + + ext/ebs.rst + ext/events.rst + ext/spritesystem.rst + ext/uisystem.rst + ext/particles.rst diff --git a/doc/modules/sdl2ext_algorithms.rst b/doc/modules/sdl2ext_algorithms.rst deleted file mode 100644 index 2d43ce3f..00000000 --- a/doc/modules/sdl2ext_algorithms.rst +++ /dev/null @@ -1,42 +0,0 @@ -.. currentmodule:: sdl2.ext - -Common algorithms -================= - -.. function:: cohensutherland(left : int, top : int, right : int, \ - bottom : int, x1 : int, y1 : int, x2 : int, y2 : int) -> int, int, int, int - - This implements the Cohen-Sutherland line clipping - algorithm. *left*, *top*, *right* and *bottom* denote the - clipping area, into which the line defined by *x1*, *y1* (start - point) and *x2*, *y2* (end point) will be clipped. - - If the line does not intersect with the rectangular clipping area, - four ``None`` values will be returned as tuple. Otherwise a tuple of - the clipped line points will be returned in the form ``(cx1, cy1, - cx2, cy2)``. - -.. function:: liangbarsky(left : int, top : int, right : int, \ - bottom : int, x1 : int, y1 : int, x2 : int, y2 : int) -> int, int, int, int - - This implements the Liang-Barsky line clipping algorithm. *left*, - *top*, *right* and *bottom* denote the clipping area, into - which the line defined by *x1*, *y1* (start point) and *x2*, - *y2* (end point) will be clipped. - - If the line does not intersect with the rectangular clipping area, - four ``None`` values will be returned as tuple. Otherwise a tuple of - the clipped line points will be returned in the form ``(cx1, cy1, - cx2, cy2)``. - -.. function:: clipline(left : int, top : int, right : int, \ - bottom : int, x1 : int, y1 : int, x2 : int, \ - y2 : int[,method=liangbarsky]) -> int, int, int, int - - Clips a line to a rectangular area. - -.. function:: point_on_line(p1 : iterable, p2 : iterable, point : iterable) -> bool - - Checks, if *point*, a two-value tuple, is on the line segment defined by *p1* - and *p2*. - diff --git a/doc/modules/sdl2ext_array.rst b/doc/modules/sdl2ext_array.rst deleted file mode 100644 index c02915a5..00000000 --- a/doc/modules/sdl2ext_array.rst +++ /dev/null @@ -1,276 +0,0 @@ -.. currentmodule:: sdl2.ext - -Converting sequences -==================== -This module provides various functions and classes to access sequences and -buffer-style objects in different ways. It also provides conversion routines -to improve the interoperability of sequences with :mod:`ctypes` data types. - -Providing read-write access for sequential data ------------------------------------------------ -Two classes allow you to access sequential data in different ways. The -:class:`CTypesView` provides byte-wise access to iterable objects and allows -you to convert the object representation to matching byte-widths for -:mod:`ctypes` or other modules. - -Depending on the the underlying object and the chosen size of each particular -item of the object, the :class:`CTypesView` allows you to operate directly -on different representations of the object's contents. :: - - >>> text = bytearray("Hello, I am a simple ASCII string!") - >>> ctview = CTypesView(text, itemsize=1) - >>> ctview.view[0] = 0x61 - >>> print(text) - aello, I am a simple ASCII string!" - >>> ctview.to_uint16()[3] = 0x6554 - >>> print(text) - aello,Te am a simple ASCII string!" - -The snippet above provides a single-byte sized view on a :func:`bytearray` -object. Afterwards, the first item of the view is changed, which causes a -change on the :func:`bytearray`, on the first item as well, since both, the -:class:`CTypesView` and the :func:`bytearray` provide a byte-wise access to -the contents. - -By using :meth:`CTypesView.to_uint16()`, we change the access representation to -a 2-byte unsigned integer :mod:`ctypes` pointer and change the fourth 2-byte -value, *I* to something else. :: - - >>> text = bytearray("Hello, I am a simple ASCII string!") - >>> ctview = CTypesView(text, itemsize=2) - >>> ctview.view[0] = 0x61 - >>> print(text) - aello, I am a simple ASCII string!" - >>> ctview.to_uint16()[3] = 0x6554 - >>> print(text) aello,Te am a simple ASCII string!" - -If the encapsuled object does not provide a (writable) :func:`buffer` -interface, but is iterable, the :class:`CTypesView` will create an -internal copy of the object data using Python's :mod:`array` module and -perform all operations on that copy. :: - - >>> mylist = [18, 52, 86, 120, 154, 188, 222, 240] - >>> ctview = CTypesView(mylist, itemsize=1, docopy=True) - >>> print(ctview.object) - array('B', [18, 52, 86, 120, 154, 188, 222, 240]) - >>> ctview.view[3] = 0xFF - >>> print(mylist) - [18, 52, 86, 120, 154, 188, 222, 240] - >>> print(ctview.object) - array('B', [18, 52, 86, 255, 154, 188, 222, 240]) - -As for directly accessible objects, you can define your own itemsize to -be used. If the iterable does not provide a direct byte access to their -contents, this won't have any effect except for resizing the item -widths. :: - - >>> mylist = [18, 52, 86, 120, 154, 188, 222, 240] - >>> ctview = CTypesView(mylist, itemsize=4, docopy=True) - >>> print(ctview.object) - array('I', [18L, 52L, 86L, 120L, 154L, 188L, 222L, 240L]) - -Accessing data over multiple dimensions ---------------------------------------- -The second class, :class:`MemoryView` provides an interface to access -data over multiple dimensions. You can layout and access a simple -byte stream over e.g. two or more axes, providing a greater flexibility -for functional operations and complex data. - -Let's assume, we are reading image data from a file stream into some buffer -object and want to access and manipulate the image data. Images feature two -axes, one being the width, the other being the height, defining a rectangular -graphics area. - -When we read all data from the file, we have an one-dimensional view of the -image graphics. The :class:`MemoryView` allows us to define a -two-dimensional view over the image graphics, so that we can operate on -both, rows and columns of the image. :: - - >>> imagedata = bytearray("some 1-byte graphics data") - >>> view = MemoryView(imagedata, 1, (5, 5)) - >>> print(view) - [[s, o, m, e, ], [1, -, b, y, t], [e, , g, r, a], [p, h, i, c, s], [ , d, a, t, a]] - >>> for row in view: - ... print(row) - ... - [s, o, m, e, ] - [1, -, b, y, t] - [e, , g, r, a] - [p, h, i, c, s] - [ , d, a, t, a] - >>> for row in view: - ... row[1] = "X" - ... print row - ... - [s, X, m, e, ] - [1, X, b, y, t] - [e, X, g, r, a] - [p, X, i, c, s] - [ , X, a, t, a] - >>> print(imagedata) - sXme 1XbyteXgrapXics Xata - -On accessing a particular dimension of a :class:`MemoryView`, a new -:class:`MemoryView` is created, if it does not access a single -element. :: - - >>> firstrow = view[0] - >>> type(firstrow) - - >>> type(firstrow[0]) - - -A :class:`MemoryView` features, similar to Python's builtin -:class:`memoryview`, dimensions and strides, accessible via the -:attr:`MemoryView.ndim` and :attr:`MemoryView.strides` attributes. - - >>> view.ndim - 2 - >>> view.strides - (5, 5) - -The :attr:`MemoryView.strides`, which have to be passed on creating a -new :class:`MemoryView`, define the layout of the data over different -dimensions. In the example above, we created a 5x5 two-dimensional view -to the image graphics. :: - - >>> twobytes = MemoryView(imagedata, 2, (5, 1)) - >>> print(twobytes) - [[sX, me, 1, Xb, yt], [eX, gr, ap, Xi, cs]] - - -Array API ---------- - -.. class:: CTypesView(obj : iterable[, itemsize=1[, docopy=False[, objsize=None]]]) - - A proxy class for byte-wise accessible data types to be used in - ctypes bindings. The CTypesView provides a read-write access to - arbitrary objects that are iterable. - - In case the object does not provide a :func:`buffer()` interface for - direct access, the CTypesView can copy the object's contents into an - internal buffer, from which data can be retrieved, once the necessary - operations have been performed. - - Depending on the item type stored in the iterable object, you might - need to provide a certain *itemsize*, which denotes the size per - item in bytes. The *objsize* argument might be necessary of iterables, - for which len() does not return the correct amount of objects or is not - implemented. - - .. attribute:: bytesize - - Returns the length of the encapsuled object in bytes. - - .. attribute:: is_shared - - Indicates, if changes on the CTypesView data effect the encapsuled - object directly. if not, this means that the object was copied - internally and needs to be updated by the user code outside of the - CTypesView. - - .. attribute:: object - - The encapsuled object. - - .. attribute:: view - - Provides a read-write aware view of the encapsuled object data - that is suitable for usage from :mod:`ctypes`. - - .. method:: to_bytes() -> ctypes.POINTER - - Returns a byte representation of the encapsuled object. The return - value allows a direct read-write access to the object data, if it - is not copied. The :func:`ctypes.POINTER` points to an array of - :class:`ctypes.c_ubyte`. - - .. method:: to_uint16() -> ctypes.POINTER - - Returns a 16-bit representation of the encapsuled object. The return - value allows a direct read-write access to the object data, if it - is not copied. The :func:`ctypes.POINTER` points to an array of - :class:`ctypes.c_ushort`. - - .. method:: to_uint32() -> ctypes.POINTER - - Returns a 32-bit representation of the encapsuled object. The return - value allows a direct read-write access to the object data, if it - is not copied. The :func:`ctypes.POINTER` points to an array of - :class:`ctypes.c_uint`. - - .. method:: to_uint64() -> ctypes.POINTER - - Returns a 64-bit representation of the encapsuled object. The return - value allows a direct read-write access to the object data, if it - is not copied. The :func:`ctypes.POINTER` points to an array of - :class:`ctypes.c_ulonglong`. - -.. class:: MemoryView(source : object, itemsize : int, strides : tuple[, getfunc=None[, setfunc=None[, srcsize=None]]]) - - The :class:`MemoryView` provides a read-write access to arbitrary - data objects, which can be indexed. - - *itemsize* denotes the size of a single item. *strides* defines - the dimensions and the length (n items * *itemsize*) for each - dimension. *getfunc* and *setfunc* are optional parameters to - provide specialised read and write access to the underlying - *source*. *srcsize* can be used to provide the correct source - size, if ``len(source)`` does not return the absolute size of the - source object in all dimensions. - - .. note:: - - The MemoryView is a pure Python-based implementation and makes - heavy use of recursion for multi-dimensional access. If you aim - for speed on accessing a n-dimensional object, you want to - consider using a specialised library such as numpy. If you need - n-dimensional access support, where such a library is not - supported, or if you need to provide access to objects, which do - not fulfill the requirements of that particular libray, - :class:`MemoryView` can act as solid fallback solution. - - .. attribute:: itemsize - - The size of a single item in bytes. - - .. attribute:: ndim - - The number of dimensions of the :class:`MemoryView`. - - .. attribute:: size - - The size in bytes of the underlying source object. - - .. attribute:: source - - The underlying data source. - - .. attribute:: strides - - A tuple defining the length in bytes for accessing all - elements in each dimension of the :class:`MemoryView`. - -.. function:: to_ctypes(dataseq : iterable, dtype[, mcount=0]) -> array, int - - Converts an arbitrary sequence to a ctypes array of the specified - *dtype* and returns the ctypes array and amount of items as - two-value tuple. - - Raises a :exc:`TypeError`, if one or more elements in the passed - sequence do not match the passed *dtype*. - -.. function:: to_list(dataseq : iterable) -> list - - Converts a ctypes array to a list. - -.. function:: to_tuple(dataseq : iterable) -> tuple - - Converts a ctypes array to a tuple. - -.. function:: create_array(obj : object, itemsize : int) -> array.array - - Creates an :class:`array.array` based copy of the passed object. - *itemsize* denotes the size in bytes for a single element within - *obj*. diff --git a/doc/modules/sdl2ext_color.rst b/doc/modules/sdl2ext_color.rst deleted file mode 100644 index 9c4bcb1b..00000000 --- a/doc/modules/sdl2ext_color.rst +++ /dev/null @@ -1,141 +0,0 @@ -.. currentmodule:: sdl2.ext - -Color handling -============== - -.. class:: Color(r=255, g=255, b=255, a=255) - - A simple RGBA-based color implementation. The Color class uses a - byte-wise representation of the 4 channels red, green, blue and alpha - transparency, so that the values range from 0 to 255. It allows basic - arithmetic operations, e.g. color addition or subtraction and - conversions to other color spaces such as HSV or CMY. - - .. attribute:: r - - The red channel value of the Color. - - .. attribute:: g - - The green channel value of the Color. - - .. attribute:: b - - The blue channel value of the Color. - - .. attribute:: a - - The alpha channel value of the Color. - - .. attribute:: cmy - - The CMY representation of the Color. The CMY components are in the - ranges C = [0, 1], M = [0, 1], Y = [0, 1]. Note that this will not - return the absolutely exact CMY values for the set RGB values in - all cases. Due to the RGB mapping from 0-255 and the CMY mapping - from 0-1 rounding errors may cause the CMY values to differ - slightly from what you might expect. - - .. attribute:: hsla - - The HSLA representation of the Color. The HSLA components are in - the ranges H = [0, 360], S = [0, 100], L = [0, 100], A = [0, - 100]. Note that this will not return the absolutely exact HSL - values for the set RGB values in all cases. Due to the RGB mapping - from 0-255 and the HSL mapping from 0-100 and 0-360 rounding - errors may cause the HSL values to differ slightly from what you - might expect. - - .. attribute:: hsva - - The HSVA representation of the Color. The HSVA components are in - the ranges H = [0, 360], S = [0, 100], V = [0, 100], A = [0, - 100]. Note that this will not return the absolutely exact HSV - values for the set RGB values in all cases. Due to the RGB mapping - from 0-255 and the HSV mapping from 0-100 and 0-360 rounding - errors may cause the HSV values to differ slightly from what you - might expect. - - .. attribute:: i1i2i3 - - The I1I2I3 representation of the Color. The I1I2I3 components are - in the ranges I1 = [0, 1], I2 = [-0.5, 0.5], I3 = [-0.5, - 0.5]. Note that this will not return the absolutely exact I1I2I3 - values for the set RGB values in all cases. Due to the RGB mapping - from 0-255 and the I1I2I3 from 0-1 rounding errors may cause the - I1I2I3 values to differ slightly from what you might expect. - - .. method:: normalize() -> (float, float, float, float) - - Returns the normalised RGBA values of the Color as floating point - values in the range [0, 1]. - - .. method:: __add__(self, color) -> Color - __sub__(self, color) -> Color - __mul__(self, color) -> Color - __div__(self, color) -> Color - __truediv__(self, color) -> Color - __mod__(self, color) -> Color - - Basic arithmetic functions for :class:`Color` values. The arithmetic - operations ``+, -, *, /, %`` are supported by the :class:`Color` class - and work on a per-channel basis. This means, that the operation :: - - color = color1 + color2 - - is the same as :: - - color = Color() - color.r = min(color1.r + color2.r, 255) - color.g = min(color1.g + color2.g, 255) - ... - - The operations guarantee that the channel values stay in the allowed - range of [0, 255]. - -.. function:: argb_to_color(v : int) -> Color - ARGB(v : int) -> Color - - Converts an integer value to a Color, assuming the integer represents - a 32-bit ARGB value. - -.. function:: convert_to_color(v : object) -> Color - COLOR(v : object) -> Color - - Tries to convert the passed value to a Color object. The value can be - an arbitrary Python object, which is passed to the different other - conversion functions. If one of them succeeds, the Color will be - returned to the caller. If none succeeds, a :exc:`ValueError` will be - raised. - - If the color is an integer value, it is assumed to be in ARGB layout. - -.. function:: rgba_to_color(v : int) -> Color - RGBA(v : int) -> Color - - Converts an integer value to a Color, assuming the integer represents - a 32-bit RGBA value. - -.. function:: is_rgb_color(v : object) -> bool - - Checks, if the passed value is an item that could be converted to a - RGB color. - -.. function:: is_rgba_color(v : object) -> bool - - Checks, if the passed value is an item that could be converted to a - RGBA color. - -.. function:: string_to_color(v : string) -> Color - - Converts a hex color string or color name to a Color value. Supported - hex values are: - - * #RGB - * #RGBA - * #RRGGBB - * #RRGGBBAA - * 0xRGB - * 0xRGBA - * 0xRRGGBB - * 0xRRGGBBAA diff --git a/doc/modules/sdl2ext_colorpalettes.rst b/doc/modules/sdl2ext_colorpalettes.rst deleted file mode 100644 index 12df7001..00000000 --- a/doc/modules/sdl2ext_colorpalettes.rst +++ /dev/null @@ -1,29 +0,0 @@ -.. module:: sdl2.ext.colorpalettes - :synopsis: Predefined sets of colors. - -sdl2.ext.colorpalettes - predefined sets of colors -================================================== -Indexed color palettes. Each palette is a tuple of -:class:`sdl2.ext.Color` objects. - -The following palettes are currently available: - -================== =================================================== -Palette Identifier Description -================== =================================================== -MONOPALETTE 1-bit monochrome palette (black and white). - -GRAY2PALETTE 2-bit grayscale palette with black, white and two - shades of gray. -GRAY4PALETTE 4-bit grayscale palette with black, white and 14 - shades shades of gray. -GRAY8PALETTE 8-bit grayscale palette with black, white and 254 - shades shades of gray. -RGB3PALETTE 3-bit RGB color palette with pure red, green and - blue and their complementary colors as well as black - and white. -CGAPALETTE CGA color palette. -EGAPALETTE EGA color palette. -VGAPALETTE 8-bit VGA color palette. -WEBPALETTE "Safe" web color palette with 225 colors. -================== =================================================== diff --git a/doc/modules/sdl2ext_common.rst b/doc/modules/sdl2ext_common.rst deleted file mode 100644 index 30716638..00000000 --- a/doc/modules/sdl2ext_common.rst +++ /dev/null @@ -1,43 +0,0 @@ -.. currentmodule:: sdl2.ext - -Initialization routines -======================= - -:func:`init()` simply calls :func:`SDL_Init()` to initialize only the video -subsystem. If the call fails, :exc:`SDLError` is raised. See -:ref:`pygamers_pygame` for a comparison between this function and -:func:`pygame.init()`. - - -API ---- - -.. exception:: SDLError(msg=None) - - An SDL2 specific :class:`Exception` class. if no *msg* is provided, - the message will be set to the value of :func:`sdl2.error.SDL_GetError()` - -.. function:: init() -> None - - Initialises the underlying SDL2 video subsystem. Raises a - :exc:`SDLError`, if the SDL2 video subsystem could not be - initialised. - -.. function:: quit() -> None - - Quits the underlying SDL2 video subysystem. If no other SDL2 - subsystems are active, this will also call :func:`quit()`, - :func:`sdl2.sdlttf.TTF_Quit()` and :func:`sdl2.sdlimage.IMG_Quit()`. - -.. function:: get_events() -> [SDL_Event, SDL_Event, ...] - - Gets all SDL events that are currently on the event queue. - -.. class:: TestEventProcessor() - - A simple event processor for testing purposes. - - .. method:: run(window : Window) -> None - - Starts an event loop without actually processing any event. The method - will run endlessly until a ``SDL_QUIT`` event occurs. diff --git a/doc/modules/sdl2ext_compat.rst b/doc/modules/sdl2ext_compat.rst deleted file mode 100644 index f827bbd3..00000000 --- a/doc/modules/sdl2ext_compat.rst +++ /dev/null @@ -1,86 +0,0 @@ -.. module:: sdl2.ext.compat - :synopsis: Python compatibility helpers. - -sdl2.ext.compat - Python compatibility helpers -============================================== -The :mod:`sdl2.ext.compat` module is for internal purposes of the :mod:`sdl2` -package and should not be used outside of the package. Classes, methods and -interfaces might change between versions and there is no guarantee of API -compatibility on different platforms and python implementations or between -releases. - -.. data:: ISPYTHON2 - - ``True``, if executed in a Python 2.x compatible interpreter, ``False`` - otherwise. - -.. data:: ISPYTHON3 - - ``True``, if executed in a Python 3.x compatible interpreter, ``False`` - otherwise. - -.. function:: long([x[, base]]) - - .. note:: - - Only defined for Python 3.x, for which it is the same as :func:`int()`. - -.. function:: unichr(i) - - .. note:: - - Only defined for Python 3.x, for which it is the same as :func:`chr()`. - -.. function:: unicode(string[, encoding[, errors]]) - - .. note:: - - Only defined for Python 3.x, for which it is the same as :func:`str()`. - -.. function:: callable(x) -> bool - - .. note:: - - Only defined for Python 3.x, for which it is the same as - ``isinstance(x, collections.Callable)`` - -.. function:: byteify(x : string, enc : string) -> bytes - - Converts a string to a :func:`bytes` object. - -.. function:: stringify(x : bytes, enc : string) -> string - - Converts a :func:`bytes` to a string object. - -.. function:: isiterable(x) -> bool - - Shortcut for ``isinstance(x, collections.Iterable)``. - -.. function:: platform_is_64bit() -> bool - - Checks, if the interpreter is 64-bit capable. - -.. decorator:: deprecated - - A simple decorator to mark functions and methods as deprecated. This will - print a deprecation message each time the function or method is invoked. - -.. function:: deprecation(message : string) -> None - - Prints a deprecation message using the :func:`warnings.warn()` function. - -.. exception:: UnsupportedError(obj : object[, msg=None]) - - Indicates that a certain class, function or behaviour is not supported in - the specific execution environment. - -.. decorator:: experimental - - A simple decorator to mark functions and methods as - experimental. This will print a warning each time the function or - method is invoked. - -.. exception:: ExperimentalWarning(obj : object[, msg=None]) - - Indicates that a certain class, function or behaviour is in an - experimental state. diff --git a/doc/modules/sdl2ext_draw.rst b/doc/modules/sdl2ext_draw.rst deleted file mode 100644 index 830c5008..00000000 --- a/doc/modules/sdl2ext_draw.rst +++ /dev/null @@ -1,44 +0,0 @@ -.. currentmodule:: sdl2.ext - -2D drawing routines for software surfaces -========================================= - -.. note:: - - The drawing functions within this module are unoptimised and should not be - considered fast. If you want improved drawing of 2D primitives, including - hardware acceleration, you should use the methods of the - :class:`Renderer` instead. - -.. function:: prepare_color(color : object, target : object) -> int - - Prepares the passed *color* for a specific *target*. *color* can be any - object type that can be processed by - :func:`convert_to_color()`. *target* can be any - :class:`sdl2.SDL_PixelFormat`, :class:`sdl2.SDL_Surface` or - :class:`SoftwareSprite` instance. - - The returned integer will be a color value matching the target's pixel - format. - -.. function:: fill(target : object, color : object[, area=None]) -> None - - Fills a certain area on the passed *target* with a *color*. If no *area* is - provided, the entire target will be filled with the passed color. If an - iterable item is provided as *area* (such as a list or tuple), it will be - first checked, if the item denotes a single rectangular area - (4 integer values) before assuming it to be a sequence of rectangular areas - to fill with the color. - - *target* can be any :class:`sdl2.SDL_Surface` or :class:`SoftwareSprite` - instance. - -.. function:: line(target : object, color : object[, width=1]) -> None - - Draws one or multiple lines on the passed *target*. *line* can be a - sequence of four integers for a single line in the form ``(x1, y1, - x2, y2)`` or a sequence of a multiple of 4 for drawing multiple lines - at once, e.g. ``(x1, y1, x2, y2, x3, y3, x4, y4, ...)``. - - *target* can be any :class:`sdl2.SDL_Surface` or :class:`SoftwareSprite` - instance. diff --git a/doc/modules/sdl2ext_font.rst b/doc/modules/sdl2ext_font.rst deleted file mode 100644 index cea11c2f..00000000 --- a/doc/modules/sdl2ext_font.rst +++ /dev/null @@ -1,114 +0,0 @@ -.. currentmodule:: sdl2.ext - -Text rendering routines -======================= - -.. class:: BitmapFont(surface : Sprite, size : iterable[, mapping=None) - - A bitmap graphics to character mapping. The :class:`BitmapFont` class - uses an image *surface* to find and render font character glyphs for - text. It requires a mapping table, which denotes the characters - available on the image. - - The mapping table is a list of strings, where each string reflects a - *line* of characters on the image. Each character within each line - has the same size as specified by the size argument. - - A typical mapping table might look like :: - - [ '0123456789', - 'ABCDEFGHIJ', - 'KLMNOPQRST', - 'UVWXYZ ', - 'abcdefghij', - 'klmnopqrst', - 'uvwxyz ', - ',;.:!?+-()' ] - - .. attribute:: surface - - The :class:`sdl2.SDL_Surface` containing the character bitmaps. - - .. attribute:: offsets - - A dict containing the character offsets on the :attr:`surface`. - - .. attribute:: mapping - - The character mapping table, a list of strings. - - .. attribute:: size - - The size of an individual glyph bitmap on the font. - - .. method:: render(text : string[, bpp=None]) -> Sprite - - Renders the passed text on a new :class:`Sprite` and returns it. - If no explicit *bpp* are provided, the bpp settings of the - :attr:`.surface` are used. - - .. method:: render_on(surface : Sprite, text : string[, \ - offset=(0, 0)]) -> (int, int, int, int) - - Renders a text on the passed sprite, starting at a specific - offset. The top-left start position of the text will be the - passed *offset* and a 4-value tuple with the changed area will be - returned. - - .. method:: contains(c : string) -> bool - - Checks, whether a certain character exists in the font. - - .. method:: can_render(text : string) -> bool - - Checks, whether all characters in the passed *text* can be rendered. - -.. class:: FontManager(font_path : str[, alias=None[, size=16[, color=Color(255, 255, 255)[, bg_color=Color(0, 0, 0)[, index=0]]]]]) - - Manage fonts and rendering of text. - - One font path must be given to initialise the FontManager. - :attr:`default_font` will be set to this font. *size* is the default - font size in pixels. *color* and *bg_color* will give the FontManager - a default color. *index* will select a specific font face from a file - containing multiple font faces. The first face is always at index 0. It can - be used for TTC (TrueType Font Collection) fonts. - - .. attribute:: bg_color - - The :class:`sdl2.ext.Color` to be used as background color. - - .. attribute:: color - - The :class:`sdl2.ext.Color` to be used for rendering text. - - .. attribute:: default_font - - Returns the name of the current default font being used by the - :class:`FontManager`. On assigning :attr:`default_font`, - the value must be a loaded font alias. - - .. attribute:: size - - The default font size in pixels. - - .. method:: add(font_path : str[, alias=None[, size=None[, index=0]]])) -> sdl2.sdlttf.TTF_Font - - Add a font to the :class:`FontManager`. *alias* is by default the - font name, any other name can be passed, *size* is the font size - in pixels and defaults to :attr:`size`. *index* selects a specific font - face from a TTC (TrueType Font Collection) file. Returns the font pointer - stored in :attr:`fonts`. - - .. method:: close() - - Closes all fonts used by the :class:`FontManager`. - - .. method:: render(text : str[, alias=None[, size=None[, width=None[, color=None[, bg_color=None[, **kwargs]]]]]]) -> sdl2.SDL_Surface - - Renders text to a surface. This method uses the font designated by - the passed *alias* or, if *alias* is omitted, by the set - :attr:`default_font`. A *size* can be passed even if the font was - not loaded with this size. A *width* can be given for automatic line - wrapping. If no *bg_color* or *color* are given, it will default to - the FontManager's :attr:`bg_color` and :attr:`color`. diff --git a/doc/modules/sdl2ext_image.rst b/doc/modules/sdl2ext_image.rst deleted file mode 100644 index d85f5d9e..00000000 --- a/doc/modules/sdl2ext_image.rst +++ /dev/null @@ -1,27 +0,0 @@ -.. currentmodule:: sdl2.ext - -Image loaders -============= - -.. function:: get_image_formats() -> (str, str, ...) - - Gets the formats supported by PySDL2 in the default installation. - -.. function:: load_image(fname : str[, enforce=None]) -> sdl2.SDL_Surface - - Creates a :class:`sdl2.SDL_Surface` from an image file. - - This function makes use of the `Python Imaging Library - `_, if it is available on the - target execution environment. The function will try to load the file via - :mod:`sdl2` first. If the file could not be loaded, it will try to load it - via :mod:`sdl2.sdlimage` and PIL. - - You can force the function to use only one of them, by passing the - *enforce* as either ``"PIL"`` or ``"SDL"``. - - .. note:: - - This will call :func:`sdl2.sdlimage.IMG_Init()` implicitly with the - default arguments, if the module is available and if - :func:`sdl2.SDL_LoadBMP()` failed to load the image. diff --git a/doc/modules/sdl2ext_pixelaccess.rst b/doc/modules/sdl2ext_pixelaccess.rst deleted file mode 100644 index 8b497be2..00000000 --- a/doc/modules/sdl2ext_pixelaccess.rst +++ /dev/null @@ -1,68 +0,0 @@ -.. currentmodule:: sdl2.ext - -2D and 3D direct pixel access -============================= - -.. class:: PixelView(source : object) - - 2D :class:`MemoryView` for :class:`SoftwareSprite` and - :class:`sdl2.SDL_surface` pixel access. - - .. note:: - - If necessary, the *source* surface will be locked for accessing its - pixel data. The lock will be removed once the :class:`PixelView` is - garbage-collected or deleted. - - The :class:`PixelView` uses a y/x-layout. Accessing ``view[N]`` will - operate on the Nth row of the underlying surface. To access a specific - column within that row, ``view[N][C]`` has to be used. - - .. note:: - - :class:`PixelView` is implemented on top of the :class:`MemoryView` - class. As such it makes heavy use of recursion to access rows and - columns and can be considered as slow in contrast to optimised - ndim-array solutions such as :mod:`numpy`. - -.. function:: pixels2d(source : object, transpose : bool) - - Creates a 2D pixel array, based on ``numpy.ndarray``, from the passed - *source*. *source* can be a :class:`SoftwareSprite` or - :class:`sdl2.SDL_Surface`. The ``SDL_Surface`` of the *source* will be - locked and unlocked automatically. - - By default, the returned array is formatted so that the first dimension - corresponds to height on the source and the second dimension corresponds - to width, contrary to PIL and PyOpenGL convention. To obtain an array where - the first dimension is width and second dimension is height, set the - `transpose` argument to False. - - The *source* pixels will be accessed and manipulated directly. - - .. note:: - - :func:`pixels2d` is only usable, if the numpy package is available - within the target environment. If numpy could not be imported, a - :exc:`sdl2.ext.compat.UnsupportedError` will be raised. - -.. function:: pixels3d(source : object, transpose : bool) - - Creates a 3D pixel array, based on ``numpy.ndarray``, from the passed - *source*. *source* can be a :class:`SoftwareSprite` - or :class:`sdl2.SDL_Surface`. The ``SDL_Surface`` of the *source* - will be locked and unlocked automatically. - - By default, the returned array is formatted so that the first dimension - corresponds to height on the source and the second dimension corresponds - to width, contrary to PIL and PyOpenGL convention. To obtain an array where - the first dimension is width and second dimension is height, set the - `transpose` argument to False. - - The *source* pixels will be accessed and manipulated directly. - - .. note:: - - :func:`pixels3d` is only usable, if the numpy package is available - within the target environment. If numpy could not be imported, a - :exc:`sdl2.ext.compat.UnsupportedError` will be raised. diff --git a/doc/modules/sdl2ext_resources.rst b/doc/modules/sdl2ext_resources.rst deleted file mode 100644 index a8db602a..00000000 --- a/doc/modules/sdl2ext_resources.rst +++ /dev/null @@ -1,180 +0,0 @@ -.. currentmodule:: sdl2.ext - -Resource management -=================== -Every application usually ships with various resources, such as image and data -files, configuration files and so on. Accessing those files in the folder -hierarchy or in a bundled format for various platforms can become a complex -task. The :class:`Resources` class allows you to manage different application -data in a certain directory, providing a dictionary-style access functionality -for your in-application resources. - -Let's assume, your application has the following installation layout :: - - Application Directory - Application.exe - Application.conf - data/ - background.jpg - button1.jpg - button2.jpg - info.dat - -Within the ``Application.exe`` code, you can - completely system-agnostic - -define a new resource that keeps track of all ``data`` items. :: - - apppath = os.path.dirname(os.path.abspath(__file__)) - appresources = Resources(os.path.join(apppath, "data")) - # Access some images - bgimage = appresources.get("background.jpg") - btn1image = appresources.get("button1.jpg") - ... - -To access individual files, you do not need to concat paths the whole -time and regardless of the current directory, your application operates -on, you can access your resource files at any time through the -:class:`Resources` instance, you created initially. - -The :class:`Resources` class is also able to scan an index archived files, -compressed via ZIP or TAR (gzip or bzip2 compression), and subdiectories -automatically. :: - - Application Directory - Application.exe - Application.conf - data/ - audio/ - example.wav - background.jpg - button1.jpg - button2.jpg - graphics.zip - [tileset1.bmp - tileset2.bmp - tileset3.bmp - ] - info.dat - - tilesimage = appresources.get("tileset1.bmp") - audiofile = appresources.get("example.wav") - -If you request an indexed file via :meth:`Resources.get`, you will receive -a :class:`io.BytesIO` stream, containing the file data, for further processing. - -.. note:: - - The scanned files act as keys within the :class:`Resources` class. This - means that two files, that have the same name, but are located in different - directories, will not be indexed. Only one of them will be accessible - through the :class:`Resources` class. - -API ---- - -.. class:: Resources([path=None[, subdir=None[, excludepattern=None]]]) - - The Resources class manages a set of file resources and eases - accessing them by using relative paths, scanning archives - automatically and so on. - - .. method:: add(filename : string) - - Adds a file to the resource container. Depending on the - file type (determined by the file suffix or name) the file will be - automatically scanned (if it is an archive) or checked for - availability (if it is a stream or network resource). - - .. method:: add_archive(filename : string[, typehint="zip"]) - - Adds an archive file to the resource container. This will scan the - passed archive and add its contents to the list of available and - accessible resources. - - .. method:: add_file(filename : string) - - Adds a file to the resource container. This will only add the - passed file and do not scan an archive or check the file for - availability. - - .. method:: get(filename : string) -> BytesIO - - Gets a specific file from the resource container. - - Raises a :exc:`KeyError`, if the *filename* could not be found. - - .. method:: get_filelike(filename : string) -> file object - - Similar to :meth:`get()`, but tries to return the original file - handle, if possible. If the found file is only available within an - archive, a :class:`io.BytesIO` instance will be returned. - - Raises a :exc:`KeyError`, if the *filename* could not be found. - - .. method:: get_path(filename : string) -> string - - Gets the path of the passed *filename*. If *filename* is only - available within an archive, a string in the form - ``filename@archivename`` will be returned. - - Raises a :exc:`KeyError`, if the *filename* could not be found. - - .. method:: scan(path : string[, subdir=None[, excludepattern=None]) - - Scans a path and adds all found files to the resource - container. If a file within the path is a supported archive (ZIP - or TAR), its contents will be indexed aut added automatically. - - The method will consider the directory part (``os.path.dirname``) - of the provided *path* as path to scan, if the path is not a - directory. If *subdir* is provided, it will be appended to the - path and used as starting point for adding files to the resource - container. - - *excludepattern* can be a regular expression to skip - directories, which match the pattern. - -.. function:: open_tarfile(archive : string, filename : string \ - [, directory=None[, ftype=None]]) -> BytesIO - - Opens and reads a certain file from a TAR archive. The result is - returned as :class:`BytesIO` stream. *filename* can be a relative - or absolute path within the TAR archive. The optional *directory* - argument can be used to supply a relative directory path, under which - *filename* will be searched. - - *ftype* is used to supply additional compression information, in - case the system cannot determine the compression type itself, and can - be either **"gz"** for gzip compression or **"bz2"** for bzip2 - compression. - - If the filename could not be found or an error occurred on reading it, - ``None`` will be returned. - - Raises a :exc:`TypeError`, if *archive* is not a valid TAR archive or - if *ftype* is not a valid value of ("gz", "bz2"). - - .. note:: - - If *ftype* is supplied, the compression mode will be enforced for - opening and reading. - -.. function:: open_url(filename : string[, basepath=None]) -> file object - - Opens and reads a certain file from a web or remote location. This - function utilizes the :mod:`urllib2` module for Python 2.7 and - :mod:`urllib` for Python 3.x, which means that it is restricted to - the types of remote locations supported by the module. - - *basepath* can be used to supply an additional location prefix. - -.. function:: open_zipfile(archive : string, filename : string \ - [, directory : string]) -> BytesIO - - Opens and reads a certain file from a ZIP archive. The result is - returned as :class:`BytesIO` stream. *filename* can be a relative - or absolute path within the ZIP archive. The optional *directory* - argument can be used to supply a relative directory path, under which - *filename* will be searched. - - If the filename could not be found, a :exc:`KeyError` will be raised. - Raises a :exc:`TypeError`, if *archive* is not a valid ZIP archive. diff --git a/doc/modules/sdl2ext_surface.rst b/doc/modules/sdl2ext_surface.rst deleted file mode 100644 index e9a822d3..00000000 --- a/doc/modules/sdl2ext_surface.rst +++ /dev/null @@ -1,14 +0,0 @@ -.. currentmodule:: sdl2.ext - -Software Surface manipulation -============================= - -.. function:: subsurface(surface : SDL_Surface, area : (int, int, int, int)) -> SDL_Surface - - Creates a surface from a part of another surface. The two surfaces share - pixel data. - - .. note:: - - The newly created surface *must not* be used after its parent has been - freed! \ No newline at end of file diff --git a/doc/modules/sdl2ext_window.rst b/doc/modules/sdl2ext_window.rst deleted file mode 100644 index 48f5806e..00000000 --- a/doc/modules/sdl2ext_window.rst +++ /dev/null @@ -1,88 +0,0 @@ -.. currentmodule:: sdl2.ext - -Window routines to manage on-screen windows -=========================================== - -.. class:: Window(title : string, size : iterable[, position=None[, flags=None]]) - - The Window class represents a visible on-screen object with an optional - border and *title* text. It represents an area on the screen that can be - accessed by the application for displaying graphics and receive and - process user input. - - The position to show the Window at is undefined by default, letting - the operating system or window manager pick the best location. The - behaviour can be adjusted through the ``DEFAULTPOS`` class variable. :: - - Window.DEFAULTPOS = (10, 10) - - The created Window is hidden by default, which can be overridden at - the time of creation by providing other SDL window flags through the - *flags* parameter. The default flags for creating Window instances - can be adjusted through the ``DEFAULTFLAGS`` class variable. :: - - Window.DEFAULTFLAGS = sdl2.SDL_WINDOW_SHOWN - - .. attribute:: window - - The used :class:`sdl2.SDL_Window`. - - .. attribute:: title - - The title of the :class:`Window`. - - .. attribute:: size - - The size of the :class:`Window`. - - .. attribute:: position - - The current position of the :class:`Window` top-left corner. - - .. method:: create() -> None - - Creates the underlying SDL2 window. This method does nothing, if the - window was already created. - - .. method:: open() -> None - - Creates and shows the window. - - .. method:: close() -> None - - Closes the window, implicitly destroying the underlying SDL2 window. - - .. method:: show() -> None - - Show the :class:`Window` on the display. - - .. method:: hide() -> None - - Hide the :class:`Window`. - - .. method:: maximize() -> None - - Maximizes the :class:`Window` to the display's dimensions. - - .. method:: minimize() -> None - - Minimizes the :class:`Window` to an iconified state in the system tray. - - .. method:: refresh() -> None - - Refreshes the entire :class:`Window` surface. - - .. note:: - - This only needs to be called, if a SDL_Surface was acquired via - :meth:`get_surface()` and is used to display contents. - - .. method:: get_surface() -> SDL_Surface - - Gets the :class:`sdl2.SDL_Surface` used by the :class:`Window` to - display 2D pixel data. - - .. note:: - - Using this method will make the usage of GL operations, such as - texture handling or the usage of SDL renderers impossible. diff --git a/doc/news.rst b/doc/news.rst index eb8cc9f8..2f296306 100644 --- a/doc/news.rst +++ b/doc/news.rst @@ -15,6 +15,73 @@ New Features: inline documentation, and more flexible handling of argument types and SDL errors (PR #199) * Updated to wrap new functions and constants in in SDL2 2.0.18 (PR #197) +* Documented and improved the internal :func:`sdl2.ext.byteify` and + :func:`sdl2.ext.stringify` functions. +* Re-documented and improved input handling and type checking for the + :func:`sdl2.ext.subsurface` function. +* Added automatic inference of compression format from the filenames of + .tar archives for the :mod:`sdl2.ext.resources` submodule. +* Added up-to-date documentation for the :mod:`sdl2.sdlttf` and + :mod:`sdl.sdlimage` modules. +* Added a new class :class:`sdl2.ext.Texture` for creating renderer textures + from SDL surfaces, as a basic wrapper for the :obj:`sdl2.SDL_Texture` + structure. +* Added a new function :func:`sdl2.ext.set_texture_scale_quality` that globally + sets the scaling method (nearest-neighbour, linear filtering, or anisotropic + filtering) to use for new SDL textures. +* Added a new method :meth:`sdl2.ext.Renderer.reset_logical_size` to reset a + Renderer's logical size to its original value. +* Added a new method :meth:`sdl2.ext.Renderer.destroy` to safely destroy and + free memory associated with a Renderer after it is no longer needed. +* Added support for subpixel precision (i.e. using float coordinates) + with the drawing and copying methods of the :class:`~sdl2.ext.Renderer` class + when using SDL2 2.0.10 or newer. +* Added :meth:`sdl2.ext.Renderer.blit` as an alias for the + :meth:`sdl2.ext.Renderer.copy` method. +* Added a new function :func:`sdl2.ext.surface_to_ndarray` that returns a + non-transposed copy of a given SDL surface as a 2D or 3D Numpy array. +* Added new functions :func:`sdl2.ext.load_bmp` and :func:`sdl2.ext.load_img` + for importing image files using SDL2 and SDL_image, respectively. Both new + functions automatically convert the obtained surfaces to the ARGB8888 pixel + format by default. +* Added a new function :func:`sdl2.ext.save_bmp` for saving SDL surfaces to + BMP files. +* Added a new function :func:`sdl2.ext.pillow_to_surface` for converting + :obj:`PIL.Image.Image` objects from the Pillow library to SDL surfaces. +* Added a new method :meth:`~sdl2.ext.BitmapFont.remap` to the + :obj:`~sdl2.ext.BitmapFont` class to allow specifying custom character + widths and heights for each mapped character in a bitmap font. +* Added a new argument ``line_h`` to :meth:`sdl2.ext.BitmapFont.render_on` to + allow specifying custom line heights. +* Added a new :class:`~sdl2.ext.FontTTF` class, providing a new and flexible + Pythonic wrapper around the :mod:`~sdl2.sdlttf` module for opening and + rendering text with TrueType and OpenType fonts. New features include custom + line heights for multi-line text, left/right/center justification operations + for multiline text, and specifying font sizes in units of pixels in addition + to pt. + +API Changes: + +* Updated the :meth:`~sdl2.ext.Renderer.draw_line` and + :meth:`~sdl2.ext.Renderer.draw_point` methods of the + :class:`~sdl2.ext.Renderer` class to accept coordinates as lists of ``(x, y)`` + tuples or :obj:`~sdl2.SDL_Point`s in addition to flat ``[x, y, x, y, x, y]`` + lists. +* Updated the :meth:`~sdl2.ext.Renderer.draw_rect` and + :meth:`~sdl2.ext.Renderer.fill` methods of the + :class:`~sdl2.ext.Renderer` class to accept coordinates as lists of + :obj:`~sdl2.SDL_Rects`s in addition to lists of ``(x, y, w, h)`` tuples. +* Updated the :meth:`~sdl2.ext.Renderer.copy` method of the + :class:`~sdl2.ext.Renderer` class to accept an ``(x, y)`` tuple as a + destination, inferring the destination width and height from the dimensions + of the copied texture. +* Changed the ``index`` argument for the :class:`~sdl2.ext.Renderer` class to + take the name of the reqested rendering back end as a string instead of an + index for better clarity and cross-platform consistency. +* Updated the :class:`~sdl2.ext.PixelView` class to support negative indexing + (e.g. ``arr[-1][-1]`` for the last element in a 2D array). In previous + versions, negative indices would retrieve values from undefined sections of + memory outside of the surface. Fixed Bugs: @@ -23,6 +90,38 @@ Fixed Bugs: ``TTF_RenderUTF`` instead of ``TTF_RenderUTF8``. * Fixed a bug introduced in 0.9.9 where the ``SDL_WINDOW_INPUT_GRABBED`` constant was no longer exported. +* Changed the functions in the :mod:`sdl2.ext.pixelaccess` module to no longer + try to unlock RLE surfaces once their corresponding view objects are deleted. + This prevents segmentation faults when a view is garbage-collected but the + surface has already been freed. +* Fixed a bug where the rectangle returned by + :meth:`sdl2.ext.BitmapFont.render_on` would overestimate the size of the + rendered text by one character in both width and height. +* :meth:`sdl2.ext.BitmapFont.contains` no longer assumes that the font map + contains a space. +* Rendering multiline text with the :class:`sdl2.ext.BitmapFont` class now + always splits lines using the newline (`\n`) character. Previously on + Windows, it would only split on Windows-style line endings (`\r\n`). + +Deprecation Notices: + +* The :func:`sdl2.ext.open_url` function has been deprecated. +* The :func:`sdl2.ext.load_image` function has been deprecated, as it + accidentally produces different surface formats depending on the backend used. + New projects should use the new :func:`sdl2.ext.load_img`, + :func:`sdl2.ext.load_bmp`, and/or :func:`sdl2.ext.pillow_to_surface` functions + instead. +* The :class:`~sdl2.ext.UIFactory` and :class:`~sdl2.ext.UIProcessor` classes + have been deprecated due to their complexity and maintenance burden. New + functions and classes for creating GUIs with PySDL2 may be introduced in a + future release. +* The :meth:`sdl2.ext.BitmapFont.can_render` method has been deprecated. +* The :meth:`sdl2.ext.BitmapFont.render` method has been deprecated in favor of + :meth:`sdl2.ext.BitmapFont.render_text`, which returns an SDL surface instead + of a SoftwareSprite and ensures the output surface is in ARGB8888 format by + default. +* The :class:`~sdl2.ext.FontManager` class has been deprecated in favor of the + new and more flexible :class:`~sdl2.ext.FontTTF` class. 0.9.9 @@ -34,7 +133,7 @@ New Features: * Updated to wrap new functions and constants in in SDL2 2.0.16 (PR #190) -Fixed bugs: +Fixed Bugs: * Reverted the fix for (issue #139), which inadvertantly caused a serious bug that prevented usage of any non-software renderer with windows created using diff --git a/examples/helloworld.py b/examples/helloworld.py index d98a5bd9..72b2fc1f 100644 --- a/examples/helloworld.py +++ b/examples/helloworld.py @@ -29,50 +29,43 @@ def run(): # after creation. Thus we need to tell it to be shown now. window.show() - # Create a sprite factory that allows us to create visible 2D elements - # easily. Depending on what the user chosses, we either create a factory - # that supports hardware-accelerated sprites or software-based ones. - # The hardware-accelerated SpriteFactory requres a rendering context - # (or SDL_Renderer), which will create the underlying textures for us. + # Create a Renderer for the new window, which we can use to copy and + # draw things to the screen. Renderers can use hardware-accelerated + # backends (e.g. OpenGL, Direct3D) as well as software-accelerated ones, + # depending on the flags you create it with. + renderflags = sdl2.SDL_RENDERER_SOFTWARE if "-hardware" in sys.argv: - print("Using hardware acceleration") - renderer = sdl2.ext.Renderer(window) - factory = sdl2.ext.SpriteFactory(sdl2.ext.TEXTURE, renderer=renderer) - else: - print("Using software rendering") - factory = sdl2.ext.SpriteFactory(sdl2.ext.SOFTWARE) + renderflags = ( + sdl2.SDL_RENDERER_ACCELERATED | sdl2.SDL_RENDERER_PRESENTVSYNC + ) + renderer = sdl2.ext.Renderer(window, flags=renderflags) - # Creates a simple rendering system for the Window. The - # SpriteRenderSystem can draw Sprite objects on the window. - spriterenderer = factory.create_sprite_render_system(window) + # Import an image file and convert it to a Texture. A Texture is an SDL + # surface that has been prepared for use with a given Renderer. + tst_img = sdl2.ext.load_bmp(RESOURCES.get_path("hello.bmp")) + tx = sdl2.ext.Texture(renderer, tst_img) - # Creates a new 2D pixel-based surface to be displayed, processed or - # manipulated. We will use the one of the shipped example images - # from the resource package to display. - sprite = factory.from_image(RESOURCES.get_path("hello.bmp")) + # Display the image on the window. This code takes the texture we created + # earlier and copies it to the renderer (with the top-left corner of the + # texture placed at the coordinates (0, 0) on the window surface), then + # takes the contents of the renderer surface and presents them on its + # associated window. + renderer.copy(tx, dstrect=(0, 0)) + renderer.present() - # Display the surface on the window. This will copy the contents - # (pixels) of the surface to the window. The surface will be - # displayed at surface.position on the window. Play around with the - # surface.x and surface.y values or surface.position (which is just - # surface.x and surface.y grouped as tuple)! - spriterenderer.render(sprite) + # Create a simple event loop. This fetches the SDL2 event queue and checks + # for any quit events. Once a quit event is received, the loop will end + # and we'll send the signal to quit the program. + running = True + while running: + events = sdl2.ext.get_events() + for event in events: + if event.type == sdl2.SDL_QUIT: + running = False + break - # Set up an example event loop processing system. This is a necessity, - # so the application can exit correctly, mouse movements, etc. are - # recognised and so on. The TestEventProcessor class is just for - # testing purposes and does not do anything meaningful. Take a look - # at its code to better understand how the event processing can be - # done and customized! - processor = sdl2.ext.TestEventProcessor() - - # Start the event processing. This will run in an endless loop, so - # everything following after processor.run() will not be executed - # before some quitting event is raised. - processor.run(window) - - # We called video.init(), so we have to call video.quit() as well to - # release the resources hold by the SDL DLL. + # Now that we're done with the SDL2 library, we shut it down nicely using + # the `sdl2.ext.quit` function. sdl2.ext.quit() return 0 diff --git a/examples/transfomations.py b/examples/transfomations.py index af052e26..ecc0618c 100644 --- a/examples/transfomations.py +++ b/examples/transfomations.py @@ -14,25 +14,18 @@ def run(): window = sdl2.ext.Window("Sprite Transformations", size=(800, 600)) window.show() - # Create a hardware-accelerated sprite factory. The sprite factory requires - # a rendering context, which enables it to create the underlying textures - # that serve as the visual parts for the sprites. + # Create a hardware-accelerated renderer for drawing to the new window. + # We'll also set the default scaling quality to 'best' for smoother + # edges when rendering. renderer = sdl2.ext.Renderer(window) - factory = sdl2.ext.SpriteFactory(sdl2.ext.TEXTURE, renderer=renderer) - - # Create a simple rendering system for the window. We will use it to - # display the sprites. - rendersystem = factory.create_sprite_render_system(window) - - # Create the sprite to display. - sprite = factory.from_image(RESOURCES.get_path("hello.bmp")) - - # Use the sprite.center tuple to change the center of the sprite for - # rotation. You can reset a changed center simply by assinging None to it. - # - # sprite.center = 10, 30 # Changes the center - # sprite.center = None # Resets the center + sdl2.ext.set_texture_scale_quality(method="best") + # Import an image file and convert it to a Texture for the renderer + tst_img = sdl2.ext.load_bmp(RESOURCES.get_path("hello.bmp")) + tx = sdl2.ext.Texture(renderer, tst_img) + + angle = 0 + flip = 0 running = True while running: events = sdl2.ext.get_events() @@ -41,26 +34,30 @@ def run(): running = False break elif event.type == sdl2.SDL_KEYDOWN: - # Flip the sprite over its vertical axis on pressing down or up + # Flip the sprite vertically using the up/down arrow keys if event.key.keysym.sym in (sdl2.SDLK_DOWN, sdl2.SDLK_UP): - sprite.flip ^= sdl2.SDL_FLIP_VERTICAL - # Flip the sprite over its horizontal axis on pressing left or - # right + flip ^= sdl2.SDL_FLIP_VERTICAL + # Flip the sprite horizontally using the left/right arrow keys elif event.key.keysym.sym in (sdl2.SDLK_LEFT, sdl2.SDLK_RIGHT): - sprite.flip ^= sdl2.SDL_FLIP_HORIZONTAL - # Rotate the sprite around its center on pressing plus or - # minus. The center can be changed via sprite.center. - elif event.key.keysym.sym == sdl2.SDLK_PLUS: - sprite.angle += 1.0 - if sprite.angle >= 360.0: - sprite.angle = 0.0 + flip ^= sdl2.SDL_FLIP_HORIZONTAL + # Rotate the sprite around its center using the (+/-) keys + elif event.key.keysym.sym == sdl2.SDLK_EQUALS: + angle += 1.0 + if angle >= 360.0: + angle = 0.0 elif event.key.keysym.sym == sdl2.SDLK_MINUS: - sprite.angle -= 1.0 - if sprite.angle <= 0.0: - sprite.angle = 360.0 + angle -= 1.0 + if angle <= 0.0: + angle = 360.0 + + # Clear the renderer, copy our texture to it at an offset of (100, 75) + # from the top-left corner, present it to the window, and wait 10 ms + # before moving on renderer.clear() - rendersystem.render(sprite, 100, 75) + renderer.copy(tx, dstrect=(100, 75), angle=angle, flip=flip) + renderer.present() sdl2.SDL_Delay(10) + sdl2.ext.quit() return 0 diff --git a/sdl2/audio.py b/sdl2/audio.py index 83f49c8d..38fb661b 100644 --- a/sdl2/audio.py +++ b/sdl2/audio.py @@ -201,10 +201,10 @@ class SDL_AudioStream(c_void_p): SDL_UnlockAudioDevice = _bind("SDL_UnlockAudioDevice", [SDL_AudioDeviceID]) SDL_CloseAudio = _bind("SDL_CloseAudio") SDL_CloseAudioDevice = _bind("SDL_CloseAudioDevice", [SDL_AudioDeviceID]) -SDL_QueueAudio = _bind("SDL_QueueAudio", [SDL_AudioDeviceID, c_void_p, Uint32], c_int) -SDL_DequeueAudio = _bind("SDL_DequeueAudio", [SDL_AudioDeviceID, c_void_p, Uint32], Uint32) -SDL_GetQueuedAudioSize = _bind("SDL_GetQueuedAudioSize", [SDL_AudioDeviceID], Uint32) -SDL_ClearQueuedAudio = _bind("SDL_ClearQueuedAudio", [SDL_AudioDeviceID]) +SDL_QueueAudio = _bind("SDL_QueueAudio", [SDL_AudioDeviceID, c_void_p, Uint32], c_int, added='2.0.4') +SDL_DequeueAudio = _bind("SDL_DequeueAudio", [SDL_AudioDeviceID, c_void_p, Uint32], Uint32, added='2.0.5') +SDL_GetQueuedAudioSize = _bind("SDL_GetQueuedAudioSize", [SDL_AudioDeviceID], Uint32, added='2.0.4') +SDL_ClearQueuedAudio = _bind("SDL_ClearQueuedAudio", [SDL_AudioDeviceID], added='2.0.4') SDL_NewAudioStream = _bind("SDL_NewAudioStream", [SDL_AudioFormat, Uint8, c_int, SDL_AudioFormat, Uint8, c_int], POINTER(SDL_AudioStream), added='2.0.7') SDL_AudioStreamPut = _bind("SDL_AudioStreamPut", [POINTER(SDL_AudioStream), c_void_p, c_int], c_int, added='2.0.7') SDL_AudioStreamGet = _bind("SDL_AudioStreamGet", [POINTER(SDL_AudioStream), c_void_p, c_int], c_int, added='2.0.7') diff --git a/sdl2/cpuinfo.py b/sdl2/cpuinfo.py index d6854dc4..f5ef2b2e 100644 --- a/sdl2/cpuinfo.py +++ b/sdl2/cpuinfo.py @@ -30,7 +30,7 @@ SDL_HasSSE42 = _bind("SDL_HasSSE42", None, SDL_bool) SDL_GetSystemRAM = _bind("SDL_GetSystemRAM", None, c_int) SDL_HasAVX = _bind("SDL_HasAVX", None, SDL_bool) -SDL_HasAVX2 = _bind("SDL_HasAVX2", None, SDL_bool) +SDL_HasAVX2 = _bind("SDL_HasAVX2", None, SDL_bool, added='2.0.4') SDL_HasAVX512F = _bind("SDL_HasAVX512F", None, SDL_bool, added='2.0.9') SDL_HasARMSIMD = _bind("SDL_HasARMSIMD", None, SDL_bool, added='2.0.12') SDL_HasNEON = _bind("SDL_HasNEON", None, SDL_bool, added='2.0.6') diff --git a/sdl2/ext/__init__.py b/sdl2/ext/__init__.py index d0a0c07a..51d86d69 100644 --- a/sdl2/ext/__init__.py +++ b/sdl2/ext/__init__.py @@ -13,10 +13,14 @@ from .common import * from .draw import * -from .font import * -from .gui import * +from .bitmapfont import * +from .ttf import * +from .uisystem import * +from .msgbox import * from .image import * from .pixelaccess import * +from .renderer import * from .sprite import * +from .spritesystem import * from .surface import * from .window import * diff --git a/sdl2/ext/algorithms.py b/sdl2/ext/algorithms.py index 9f39586b..a895a0ab 100644 --- a/sdl2/ext/algorithms.py +++ b/sdl2/ext/algorithms.py @@ -1,4 +1,3 @@ -"""Common algorithms.""" import sys __all__ = ["liangbarsky", "cohensutherland", "clipline", "point_on_line"] @@ -23,8 +22,8 @@ def cohensutherland(left, top, right, bottom, x1, y1, x2, y2): Returns: tuple: The start and end coordinates of the clipped line in the form - ``(cx1, cy1, cx2, cy2)``. If the line does not intersect with the - rectangular clipping area, all 4 values will be ``None``. + ``(cx1, cy1, cx2, cy2)``. If the line does not intersect with the + rectangular clipping area, all 4 values will be ``None``. """ LEFT, RIGHT, LOWER, UPPER = 1, 2, 4, 8 @@ -95,8 +94,8 @@ def liangbarsky(left, top, right, bottom, x1, y1, x2, y2): Returns: tuple: The start and end coordinates of the clipped line in the form - ``(cx1, cy1, cx2, cy2)``. If the line does not intersect with the - rectangular clipping area, all 4 values will be ``None``. + ``(cx1, cy1, cx2, cy2)``. If the line does not intersect with the + rectangular clipping area, all 4 values will be ``None``. """ dx = x2 - x1 * 1.0 @@ -154,8 +153,8 @@ def clipline(l, t, r, b, x1, y1, x2, y2, method='liangbarsky'): Returns: tuple: The start and end coordinates of the clipped line in the form - ``(cx1, cy1, cx2, cy2)``. If the line does not intersect with the - rectangular clipping area, all 4 values will be ``None``. + ``(cx1, cy1, cx2, cy2)``. If the line does not intersect with the + rectangular clipping area, all 4 values will be ``None``. """ if method == 'cohensutherland': @@ -176,7 +175,7 @@ def point_on_line(p1, p2, point): Returns: bool: ``True`` if the point falls along the line segment, otherwise - ``False``. + ``False``. """ x1, y1 = p1 diff --git a/sdl2/ext/array.py b/sdl2/ext/array.py index 9e5b888a..923d7a9b 100644 --- a/sdl2/ext/array.py +++ b/sdl2/ext/array.py @@ -1,6 +1,3 @@ -""" -Conversion routines for sequences. -""" import ctypes __all__ = ["CTypesView", "to_ctypes", "to_list", "to_tuple", "create_array", @@ -33,8 +30,8 @@ def to_ctypes(dataseq, dtype, mcount=0): Returns: tuple: A tuple in the form ``(valset, count)``, where ``valset`` is - the converted ``ctypes`` array and ``count`` is the number of - elements in the array. + the converted ``ctypes`` array and ``count`` is the number of + elements in the array. Raises: TypeError: if any elements in the passed sequence do not @@ -167,7 +164,7 @@ def to_bytes(self): Returns: :obj:`ctypes._Pointer`: A pointer to a :class:`ctypes.c_uint8` - array. + array. """ castval = ctypes.POINTER(ctypes.c_uint8 * self.bytesize) @@ -182,7 +179,7 @@ def to_uint16(self): Returns: :obj:`ctypes._Pointer`: A pointer to a :class:`ctypes.c_uint16` - array. + array. """ castval = ctypes.POINTER(ctypes.c_uint16 * (self.bytesize // 2)) @@ -197,7 +194,7 @@ def to_uint32(self): Returns: :obj:`ctypes._Pointer`: A pointer to a :class:`ctypes.c_uint32` - array. + array. """ castval = ctypes.POINTER(ctypes.c_uint32 * (self.bytesize // 4)) @@ -212,7 +209,7 @@ def to_uint64(self): Returns: :obj:`ctypes._Pointer`: A pointer to a :class:`ctypes.c_uint64` - array. + array. """ castval = ctypes.POINTER(ctypes.c_uint64 * (self.bytesize // 8)) @@ -248,7 +245,8 @@ def object(self): class MemoryView(object): """A class that provides read-write access to indexable ``ctypes`` objects. - .. note: ``MemoryView`` makes heavy use of recursion for multi-dimensional + .. note:: + ``MemoryView`` makes heavy use of recursion for multi-dimensional access, making it slow for many use-cases. For better performance, the ``numpy`` library can be used for fast access to many array-like data types, but it may support fewer types of arbitrary objects than the @@ -296,6 +294,22 @@ def _setbytes(self, start, end, value): """ self._source[start:end] = value + def _getindex(self, index): + # Perform typechecking and preprocessing of view indices + if type(index) is slice: + raise IndexError("MemoryView slicing is not currently supported.") + elif type(index) is not int: + e = "Array indices must be integers (got '{0}')." + raise TypeError(e.format(str(index))) + else: + final_idx = index + if index < 0: + final_idx = len(self) + index # Handle negative indexing + if index >= len(self): + e = "Index {0} is out of bounds for a view of length {1}." + raise IndexError(e.format(index, len(self))) + return final_idx + def __len__(self): """The length of the MemoryView over the current dimension (amount of items for the current dimension). @@ -313,50 +327,42 @@ def __repr__(self): def __getitem__(self, index): """Returns the item at the specified index.""" - if type(index) is slice: - raise IndexError("slicing is not supported") + index = self._getindex(index) + if self.ndim == 1: + offset = self._offset + index * self.itemsize + return self._getfunc(offset, offset + self.itemsize) else: - if index >= len(self): - raise IndexError("index '%d'is out of bounds for '%d'" % - (index, len(self))) - if self.ndim == 1: - offset = self._offset + index * self.itemsize - return self._getfunc(offset, offset + self.itemsize) - else: - advance = self.itemsize - for b in self.strides[1:]: - advance *= b - offset = self._offset + advance * index - view = MemoryView(self._source, self.itemsize, - self.strides[1:], self._getfunc, - self._setfunc, self._srcsize) - view._offset = offset - return view + advance = self.itemsize + for b in self.strides[1:]: + advance *= b + offset = self._offset + advance * index + view = MemoryView( + self._source, self.itemsize, self.strides[1:], + self._getfunc, self._setfunc, self._srcsize + ) + view._offset = offset + return view def __setitem__(self, index, value): """Sets the item at index to the specified value.""" - if type(index) is slice: - raise IndexError("slicing is not supported") + index = self._getindex(index) + offset = self._offset + index * self.itemsize + if self.ndim == 1: + self._setfunc(offset, offset + self.itemsize, value) else: - if index >= len(self): - raise IndexError("index '%d'is out of bounds for '%d'" % - (index, len(self))) - offset = self._offset + index * self.itemsize - if self.ndim == 1: - self._setfunc(offset, offset + self.itemsize, value) - else: - advance = self.itemsize - for b in self.strides[1:]: - advance *= b - offset = self._offset + advance * index - view = MemoryView(self._source, self.itemsize, - self.strides[1:], self._getfunc, - self._setfunc, self._srcsize) - view._offset = offset - if len(value) != len(view): - raise ValueError("value does not match the view strides") - for x in range(len(view)): - view[x] = value[x] + advance = self.itemsize + for b in self.strides[1:]: + advance *= b + offset = self._offset + advance * index + view = MemoryView( + self._source, self.itemsize, self.strides[1:], + self._getfunc, self._setfunc, self._srcsize + ) + view._offset = offset + if len(value) != len(view): + raise ValueError("value does not match the view strides") + for x in range(len(view)): + view[x] = value[x] @property def size(self): diff --git a/sdl2/ext/bitmapfont.py b/sdl2/ext/bitmapfont.py new file mode 100644 index 00000000..eb8312a0 --- /dev/null +++ b/sdl2/ext/bitmapfont.py @@ -0,0 +1,289 @@ +import os +from .. import surface, rect, pixels +from .common import SDLError, raise_sdl_err +from .sprite import SoftwareSprite +from .surface import _get_target_surface +from .image import load_bmp + +__all__ = ["BitmapFont"] + + +class BitmapFont(object): + """A class for rendering text using a given bitmap font. + + This class takes an image of equally-spaced font characters and a font map + indicating the location of each character on the image, and uses these to + render text using the given font. This class is based on base SDL2 functions + and does not require the SDL_ttf library to be installed. + + The font mapping table is a list of strings, with each string representing + a row of characters on the font image surface. Each character within each + line is assumed to be of equal height and width, but this can be adjusted + using the :meth:`remap` method. + + For example, the built-in bitmap font ``font.bmp`` has the following layout: + + .. image:: images/font.png + + The default font mapping table, which matches the layout of ``font.bmp``, + looks like this:: + + fontmap = [ + '0123456789', + 'ABCDEFGHIJ', + 'KLMNOPQRST', + 'UVWXYZ ', + 'abcdefghij', + 'klmnopqrst', + 'uvwxyz ', + ',;.:!?+-()', + ] + + Args: + font_img (:obj:`SDL_Surface` or str): A surface or path to a file + containing a valid bitmap (``.bmp``) font image. + size (tuple, optional): A ``(width, height)`` tuple defining the size of + each character in the bitmap font. If not specified, this will be + inferred automatically from the fontmap and font image. + fontmap (list, optional): A list of strings defining the locations of + characters in the font image. If not specified, the default font map + defined above will be used. + + """ + DEFAULTMAP = [ + "0123456789", + "ABCDEFGHIJ", + "KLMNOPQRST", + "UVWXYZ ", + "abcdefghij", + "klmnopqrst", + "uvwxyz ", + ",;.:!?+-()", + ] + + def __init__(self, font_img, size=None, mapping=None): + if hasattr(font_img, "upper"): # if string + self.surface = load_bmp(font_img) + elif isinstance(font_img, SoftwareSprite): + self.surface = font_img.surface + self._sprite = font_img # prevent GC on the Sprite + elif isinstance(font_img, surface.SDL_Surface): + self.surface = font_img + elif "SDL_Surface" in str(type(font_img)): + self.surface = font_img.contents + else: + raise TypeError("font_img must be a Sprite or SDL_Surface") + + if mapping is None: + self.mapping = list(BitmapFont.DEFAULTMAP) + else: + self.mapping = mapping + + if not size: + map_rows = len(self.mapping) + map_cols = len(self.mapping[0]) + surf_size = (float(self.surface.w), float(self.surface.h)) + size = (int(surf_size[0] / map_cols), int(surf_size[1] / map_rows)) + self.size = size[0], size[1] + + self.offsets = {} + self._max_height = self.size[1] + self._calculate_offsets() + + def _calculate_offsets(self): + # Calculates the internal character offsets for each line + self.offsets = {} + x, y = 0, 0 + w, h = self.size + for line in self.mapping: + x = 0 + for c in line: + if c not in self.offsets.keys(): + self.offsets[c] = rect.SDL_Rect(x, y, w, h) + x += w + y += h + + def _validate_chars(self, text): + e = "The character '{0}' does not exist within the current font mapping" + for ch in text: + if ch != "\n" and ch not in self.offsets.keys(): + raise ValueError(e.format(ch)) + + def _get_rendered_size(self, text, line_h): + line_h = self._max_height if not line_h else line_h + text_w, text_h = (0, 0) + lines = text.split("\n") + for line in lines: + line_w = 0 + for c in line: + charsize = self.offsets[c] + line_w += charsize.w + if line_w > text_w: + text_w = line_w + text_h += line_h + return (text_w, text_h) + + def _render_text(self, target, fontsf, lines, line_h, offset=(0, 0)): + line_h = self._max_height if not line_h else line_h + dstr = rect.SDL_Rect(0, 0, 0, 0) + y = offset[1] + for line in lines: + x = offset[0] + for c in line: + dstr.x = x + dstr.y = y + (line_h - self.offsets[c].h) + surface.SDL_BlitSurface(fontsf, self.offsets[c], target, dstr) + x += self.offsets[c].w + y += line_h + return (x, y) + + def remap(self, c, x, y, w, h): + """Updates the source rectangle for a given font character. + + This method can be used to fine-tune the character mappings in the font + image to produce better spacing in the rendered text. + + Args: + c (str): The character to remap in the font image. + x (int): The x coordinate (in pixels) of the top-left corner of the + new rectangle for the character. + y (int): The y coordinate (in pixels) of the top-left corner of the + new rectangle for the character. + w (int): The width (in pixels) of the new rectangle for the + character. + h (int): The height (in pixels) of the new rectangle for the + character. + + """ + if len(c) > 1: + raise ValueError("Can only remap one character at a time.") + self._validate_chars(c) + x, y, w, h = [int(i) for i in (x, y, w, h)] + if any([w < 1, h < 1]): + raise ValueError("Width and height must both be positive integers.") + surf_w, surf_h = (self.surface.w, self.surface.h) + if x < 0 or y < 0 or x+w >= surf_w or y+h >= surf_h: + e = "Character rectangle cannot exceed the bounds of the font image" + raise ValueError(e + " ({0}, {1}).".format(surf_w, surf_h)) + + self.offsets[c] = rect.SDL_Rect(x, y, w, h) + if h > self._max_height: + self._max_height = h + + def render(self, text, bpp=None): + # Deprecated: replaced by render_text, which returns a surface + self._validate_chars(text) + lines = text.split("\n") + + tw, th = self._get_rendered_size(text, None) + if bpp is None: + bpp = self.surface.format.contents.BitsPerPixel + sf = surface.SDL_CreateRGBSurface(0, tw, th, bpp, 0, 0, 0, 0) + if not sf: + raise SDLError() + imgsurface = SoftwareSprite(sf.contents, False) + + self._render_text(imgsurface.surface, self.surface, lines, None) + return imgsurface + + def render_text(self, text, line_h=None, as_argb=True): + """Renders a string of text to a new surface. + + If a newline character (``\\n``) is encountered in the string, it will + be rendered as a line break in the rendered text. + + By default, this function also converts the rendered text from the native + format of the font image to 32-bit ARGB, for consistency across functions + and better compatibility with SDL2 renderers. To disable ARGB conversion, + set the ``as_argb`` parameter to ``False``. + + Args: + text (str): The string of text to render. + line_h (int, optional): The line height (in pixels) to use for each + line of the rendered text. If not specified, the maximum + character height for the font will be used. Defaults to ``None``. + as_argb (bool, optional): Whether the output surface should be + converted to 32-bit ARGB pixel format or left as-is. Defaults to + ``True`` (convert to ARGB). + + Returns: + :obj:`~sdl2.SDL_Surface`: A surface containing the rendered text. + + """ + self._validate_chars(text) + lines = text.split("\n") + + # Create a new surface with the same format as the font image + tw, th = self._get_rendered_size(text, line_h) + fmt = self.surface.format.contents.format + bpp = 32 # according to SDL2 source, this has no effect + sf = surface.SDL_CreateRGBSurfaceWithFormat(0, tw, th, bpp, fmt) + if not sf: + raise_sdl_err("creating the font surface") + + # Render text to the new surface, converting pixel format if necessary + self._render_text(sf.contents, self.surface, lines, line_h) + if as_argb and fmt != pixels.SDL_PIXELFORMAT_ARGB8888: + out_fmt = pixels.SDL_AllocFormat(pixels.SDL_PIXELFORMAT_ARGB8888) + sf_argb = surface.SDL_ConvertSurface(sf, out_fmt, 0) + surface.SDL_FreeSurface(sf) + sf = sf_argb + if not sf: + raise_sdl_err("converting rendered text to ARGB format") + + return sf.contents + + def render_on(self, target, text, offset=(0, 0), line_h=None): + """Renders a string of text to an existing surface. + + If a newline character (``\\n``) is encountered in the string, it will + be rendered as a line break in the rendered text. + + Args: + target (:obj:`~sdl2.SDL_Surface`): The surface on which to render + the given string. + text (str): The string of text to render to the target surface. + offset (tuple, optional): The ``(x, y)`` coordinates of the target + surface on which the top-left corner of the rendered text will + be placed. Defaults to ``(0, 0)``. + line_h (int, optional): The line height (in pixels) to use for each + line of the rendered text. If not specified, the maximum + character height for the font will be used. Defaults to ``None``. + + Returns: + tuple: The ``(x1, y1, x2, y2)`` rectangle of the target surface on + which the text was rendered. + + """ + x1, y1 = offset + dest = rect.SDL_Rect(x1, y1, 0, 0) + target = _get_target_surface(target) + + sf = self.render_text(text, line_h, as_argb=False) + ret = surface.SDL_BlitSurface(sf, None, target, dest) + if ret != 0: + raise_sdl_err("copying the text to the target surface") + + return (x1, y1, x1 + sf.w, y1 + sf.h) + + def contains(self, c): + """Checks whether a given character is mapped within the font. + + Args: + c (str): The character to check for within the font map. + + Returns: + bool: ``True`` if the font contains the character, otherwise + ``False``. + + """ + return c in self.offsets + + def can_render(self, text): + # Deprecated: already throws informative exception on missing character + lines = text.split("\n") + for line in lines: + for c in line: + if c != ' ' and c not in self.offsets: + return False + return True diff --git a/sdl2/ext/color.py b/sdl2/ext/color.py index 842f5a20..e1e6755d 100644 --- a/sdl2/ext/color.py +++ b/sdl2/ext/color.py @@ -1,10 +1,6 @@ -""" -color module for color creation and conversion operations. -""" from math import floor from .compat import * - __all__ = ["Color", "is_rgb_color", "is_rgba_color", "argb_to_color", "ARGB", "rgba_to_color", "RGBA", "string_to_color", "convert_to_color", "COLOR"] @@ -455,7 +451,7 @@ def is_rgb_color(v): Returns: bool: True if the value can be interpreted as an RGB color, otherwise - False. + False. """ try: @@ -482,7 +478,7 @@ def is_rgba_color(v): Returns: bool: True if the value can be interpreted as an RGBA color, otherwise - False. + False. """ rgb = is_rgb_color(v) diff --git a/sdl2/ext/colorpalettes.py b/sdl2/ext/colorpalettes.py index ef73e073..066153b8 100644 --- a/sdl2/ext/colorpalettes.py +++ b/sdl2/ext/colorpalettes.py @@ -1,32 +1,33 @@ -""" -Various indexed color palettes. +"""This module defines a number of common color palettes. -Each palette is a tuple of :obj:~`sdl2.ext.Color` objects. The following +Each palette is a tuple of :obj:`~sdl2.ext.Color` objects. The following palettes are currently available: +--------------------+---------------------------------------------------+ -| MONOPALETTE | 1-bit monochrome palette (black and white). | +| Palette | Description | ++====================+===================================================+ +| ``MONOPALETTE`` | 1-bit monochrome palette (black and white). | +--------------------+---------------------------------------------------+ -| GRAY2PALETTE | 2-bit grayscale palette with black, white and two | +| ``GRAY2PALETTE`` | 2-bit grayscale palette with black, white and two | | | shades of gray. | +--------------------+---------------------------------------------------+ -| GRAY4PALETTE | 4-bit grayscale palette with black, white and | +| ``GRAY4PALETTE`` | 4-bit grayscale palette with black, white and | | | 14 shades shades of gray. | +--------------------+---------------------------------------------------+ -| GRAY8PALETTE | 8-bit grayscale palette with black, white and | +| ``GRAY8PALETTE`` | 8-bit grayscale palette with black, white and | | | 254 shades shades of gray. | +--------------------+---------------------------------------------------+ -| RGB3PALETTE | 3-bit RGB color palette with pure red, green and | +| ``RGB3PALETTE`` | 3-bit RGB color palette with pure red, green and | | | blue and their complementary colors as well as | | | black and white. | +--------------------+---------------------------------------------------+ -| CGAPALETTE | CGA color palette. | +| ``CGAPALETTE`` | CGA color palette. | +--------------------+---------------------------------------------------+ -| EGAPALETTE | EGA color palette. | +| ``EGAPALETTE`` | EGA color palette. | +--------------------+---------------------------------------------------+ -| VGAPALETTE | 8-bit VGA color palette. | +| ``VGAPALETTE`` | 8-bit VGA color palette. | +--------------------+---------------------------------------------------+ -| WEBPALETTE | "Safe" web color palette with 225 colors. | +| ``WEBPALETTE`` | "Safe" web color palette with 225 colors. | +--------------------+---------------------------------------------------+ """ @@ -48,14 +49,14 @@ def _create_8bpp_gray(): GRAY2PALETTE = ( ARGB(0xFF000000), ARGB(0xFF555555), ARGB(0xFFAAAAAA), ARGB(0xFFFFFFFF), - ) +) GRAY4PALETTE = ( ARGB(0xFF000000), ARGB(0xFF111111), ARGB(0xFF222222), ARGB(0xFF333333), ARGB(0xFF444444), ARGB(0xFF555555), ARGB(0xFF666666), ARGB(0xFF777777), ARGB(0xFF888888), ARGB(0xFF999999), ARGB(0xFFAAAAAA), ARGB(0xFFBBBBBB), ARGB(0xFFCCCCCC), ARGB(0xFFDDDDDD), ARGB(0xFFEEEEEE), ARGB(0xFFFFFFFF), - ) +) GRAY8PALETTE = _create_8bpp_gray() @@ -64,7 +65,7 @@ def _create_8bpp_gray(): ARGB(0xFFAA0000), ARGB(0xFFAA00AA), ARGB(0xFFAA5500), ARGB(0xFFAAAAAA), ARGB(0xFF555555), ARGB(0xFF5555FF), ARGB(0xFF55FF55), ARGB(0xFF55FFFF), ARGB(0xFFFF5555), ARGB(0xFFFF55FF), ARGB(0xFFFFFF55), ARGB(0xFFFFFFFF), - ) +) EGAPALETTE = ( ARGB(0xFF000000), ARGB(0xFF0000AA), ARGB(0xFF00AA00), ARGB(0xFF00AAAA), @@ -83,7 +84,7 @@ def _create_8bpp_gray(): ARGB(0xFFFF5500), ARGB(0xFFFF55AA), ARGB(0xFFFFFF00), ARGB(0xFFFFFFAA), ARGB(0xFF555555), ARGB(0xFF5555FF), ARGB(0xFF55FF55), ARGB(0xFF55FFFF), ARGB(0xFFFF5555), ARGB(0xFFFF55FF), ARGB(0xFFFFFF55), ARGB(0xFFFFFFFF), - ) +) WEBPALETTE = ( ARGB(0xFFFFFFFF), ARGB(0xFFFFFFCC), ARGB(0xFFFFFF99), ARGB(0xFFFFFF66), @@ -140,12 +141,12 @@ def _create_8bpp_gray(): ARGB(0xFF0033FF), ARGB(0xFF0033CC), ARGB(0xFF003399), ARGB(0xFF003366), ARGB(0xFF003333), ARGB(0xFF003300), ARGB(0xFF0000FF), ARGB(0xFF0000CC), ARGB(0xFF000099), ARGB(0xFF000066), ARGB(0xFF000033), ARGB(0xFF000000), - ) +) RGB3PALETTE = ( ARGB(0xFF000000), ARGB(0xFF0000FF), ARGB(0xFF00FF00), ARGB(0xFF00FFFF), ARGB(0xFFFF0000), ARGB(0xFFFF00FF), ARGB(0xFFFFFF00), ARGB(0xFFFFFFFF), - ) +) VGAPALETTE = ( ARGB(0xFF000000), ARGB(0xFF0000AA), ARGB(0xFF00AA00), ARGB(0xFF00AAAA), @@ -212,4 +213,4 @@ def _create_8bpp_gray(): ARGB(0xFF2D4141), ARGB(0xFF2D3D41), ARGB(0xFF2D3541), ARGB(0xFF2D3141), ARGB(0xFF000000), ARGB(0xFF000000), ARGB(0xFF000000), ARGB(0xFF000000), ARGB(0xFF000000), ARGB(0xFF000000), ARGB(0xFF000000), ARGB(0xFF000000), - ) +) diff --git a/sdl2/ext/common.py b/sdl2/ext/common.py index f883963d..d2b35da2 100644 --- a/sdl2/ext/common.py +++ b/sdl2/ext/common.py @@ -1,4 +1,3 @@ -"""SDL2 helper functions.""" import ctypes from .. import SDL_Init, SDL_Quit, SDL_QuitSubSystem, SDL_WasInit, \ SDL_INIT_VIDEO, error, events, timer @@ -37,11 +36,26 @@ def __str__(self): return repr(self.msg) +def raise_sdl_err(desc=None): + # Raises and clears the latest SDL error. For internal use. + errmsg = error.SDL_GetError().decode('utf-8') + error.SDL_ClearError() + e = "Error encountered" + if desc: + e += " " + desc + if len(errmsg): + e += ": {0}".format(errmsg) + raise SDLError(e) + + def init(): """Initializes the SDL2 video subsystem. - Raises an :exc:`SDLError` if the SDL2 video subsystem cannot be - initialized. + See :ref:`pygamers_pygame` for a comparison between this function and + ``pygame.init()``. + + Raises: + :exc:`SDLError`: If the SDL2 video subsystem cannot be initialized. """ # TODO: More subsystems? @@ -60,7 +74,7 @@ def quit(): # TODO: More subsystems? Also, is TTF_WasInit always 1? SDL_QuitSubSystem(SDL_INIT_VIDEO) if SDL_WasInit(0) != 0: - if _HASSDLTTF and sdlttf.TTF_WasInit() == 1: + if _HASSDLTTF and sdlttf.TTF_WasInit() > 0: sdlttf.TTF_Quit() if _HASSDLIMAGE: sdlimage.IMG_Quit() @@ -72,7 +86,7 @@ def get_events(): Returns: :obj:`List`: A list of all :obj:`~sdl2.SDL_Event` objects currently in - the event queue. + the event queue. """ events.SDL_PumpEvents() diff --git a/sdl2/ext/compat.py b/sdl2/ext/compat.py index d3b5b412..71b7ed1d 100644 --- a/sdl2/ext/compat.py +++ b/sdl2/ext/compat.py @@ -1,6 +1,3 @@ -""" -Python compatibility helpers. -""" import sys import warnings try: @@ -8,8 +5,8 @@ except ImportError: from collections import Callable, Iterable -__all__ = ["stringify", "byteify", "isiterable", "ISPYTHON2", "ISPYTHON3", - "platform_is_64bit", "deprecated", "deprecation", +__all__ = ["ISPYTHON2", "ISPYTHON3", "utf8", "stringify", "byteify", + "isiterable", "platform_is_64bit", "deprecated", "deprecation", "UnsupportedError", "ExperimentalWarning", "experimental", ] @@ -17,50 +14,114 @@ ISPYTHON3 = False if sys.version_info[0] < 3: - # Wrapper around bytes() and decode() for Python 2.x - byteify = lambda x, enc: x.encode(enc) - # Wrapper around str() for Python 2.x - stringify = lambda x, enc: str(x) ISPYTHON2 = True else: __all__ += ["long", "unichr", "callable", "unicode"] - byteify = bytes - stringify = lambda x, enc: x.decode(enc) + ISPYTHON3 = True long = int unichr = chr callable = lambda x: isinstance(x, Callable) - ISPYTHON3 = True unicode = str -def isiterable(x): - """Determines if an object is iterable and not a string.""" - return hasattr(x, "__iter__") and not hasattr(x, "upper") +def _to_unicode(x, enc): + if ISPYTHON2: + if type(x) in (str, bytes): + return x.decode(enc) + else: + return unicode(x) + else: + if type(x) == bytes: + return x.decode(enc) + else: + return str(x) def utf8(x): """Converts input to a unicode string in a Python 2/3 agnostic manner. + If a :obj:`bytes` object is passed, it will be decoded as UTF-8. This + function returns :obj:`unicode` for Python 2 and :obj:`str` for Python 3. + + Args: + x: Input to convert to a unicode string. + + Returns: + :obj:`str` on Python 3.x, or :obj:`unicode` on Python 2.7. + + """ + return _to_unicode(x, 'utf-8') + + +def stringify(x, enc='utf-8'): + """Converts input to a :obj:`str` in a Python 2/3 agnostic manner. + + If the input is :obj:`unicode` and the Python version is 2.7, the ``enc`` + parameter indicates the encoding to use when converting the input to + a non-unicode string. If the input is :obj:`bytes` and the Python version + is 3.x, the ``enc`` parameter indicates the encoding to use to decode the + input into a unicode string. + + Args: + x: Input to convert to a :obj:`str`. + enc (str, optional): The encoding type used to encode or decode the + input, depending on the input type and the major Python version. + Defaults to UTF-8. + """ if ISPYTHON2: - if type(x) in (str, bytes): - return x.decode('utf-8') - else: - return unicode(x) - else: - if type(x) == bytes: - return x.decode('utf-8') + if type(x) == unicode: + return x.encode(enc) else: return str(x) + else: + return _to_unicode(x, enc) + + +def byteify(x, enc='utf-8'): + """Converts input to :obj:`bytes` in a Python 2/3 agnostic manner. + + If the input is a unicode string, the ``enc`` parameter indicates + the encoding to use when encoding the input to :obj:`bytes`. + + Args: + x: Input to convert to :obj:`bytes`. + enc (str, optional): The encoding type used to encode any unicode + string input. Defaults to UTF-8. + + """ + unicode_str = unicode if ISPYTHON2 else str + if type(x) == unicode_str: + return x.encode(enc) + else: + return bytes(x) + + +def isiterable(x): + """Checks whether the input is a non-string iterable. + + Args: + x: The object to check for iterability. + + Returns: + bool: True if the input is a valid iterable, otherwise False. + + """ + return hasattr(x, "__iter__") and not hasattr(x, "upper") def platform_is_64bit(): - """Checks, if the platform is a 64-bit machine.""" + """Checks whether the Python interpreter is 64-bit. + + Returns: + bool: True if running on 64-bit Python, otherwise False. + + """ return sys.maxsize > 2 ** 32 def deprecated(func): - """A simple decorator to mark functions and methods as deprecated.""" + # A simple decorator to mark functions and methods as deprecated def wrapper(*fargs, **kw): warnings.warn("%s is deprecated." % func.__name__, category=DeprecationWarning, stacklevel=2) @@ -72,34 +133,17 @@ def wrapper(*fargs, **kw): def deprecation(message): - """Prints a deprecation message.""" + # Prints a deprecation message warnings.warn(message, category=DeprecationWarning, stacklevel=2) -class UnsupportedError(Exception): - """Indicates that a certain class, function or behaviour is not - supported. - """ - def __init__(self, obj, msg=None): - """Creates an UnsupportedError for the specified obj. - - If a message is passed in msg, it will be printed instead of the - default message. - """ - super(UnsupportedError, self).__init__() - self.obj = obj - self.msg = msg - - def __str__(self): - if self.msg is None: - return "'%s' is not supported" % repr(self.obj) - return repr(self.msg) +class UnsupportedError(RuntimeError): + # Indicates that a certain class, function or behaviour is not supported + pass class ExperimentalWarning(Warning): - """Indicates that a certain class, function or behaviour is in an - experimental state. - """ + # Indicates that a certain class, function or behaviour is experimental def __init__(self, obj, msg=None): """Creates a ExperimentalWarning for the specified obj. @@ -117,7 +161,7 @@ def __str__(self): def experimental(func): - """A simple decorator to mark functions and methods as experimental.""" + # A simple decorator to mark functions and methods as experimental def wrapper(*fargs, **kw): warnings.warn("%s" % func.__name__, category=ExperimentalWarning, stacklevel=2) diff --git a/sdl2/ext/draw.py b/sdl2/ext/draw.py index 17bc7ae0..42b18806 100644 --- a/sdl2/ext/draw.py +++ b/sdl2/ext/draw.py @@ -1,46 +1,35 @@ -"""Drawing routines for software surfaces.""" import ctypes from .compat import isiterable, UnsupportedError from .array import to_ctypes from .color import convert_to_color from .. import surface, pixels, rect from .algorithms import clipline -from .sprite import SoftwareSprite +from .surface import _get_target_surface __all__ = ["prepare_color", "fill", "line"] -def _get_target_surface(target, argname="target"): - """Gets the SDL_surface from the passed target.""" - if isinstance(target, surface.SDL_Surface): - rtarget = target - elif isinstance(target, SoftwareSprite): - rtarget = target.surface - elif "SDL_Surface" in str(type(target)): - rtarget = target.contents - else: - raise TypeError("{0} must be a Sprite or SDL_Surface".format(argname)) - return rtarget - - def prepare_color(color, target): """Prepares a given color for a specific target. + Targets can be :obj:`~sdl2.SDL_PixelFormat`, :obj:`~sdl2.SDL_Surface`, + or :obj:`~sdl2.ext.SoftwareSprite` objects. + Colors can be provided in any form supported by :func:`sdl2.ext.convert_to_color`. Args: color (:obj:`sdl2.ext.Color`): The color to prepare for the pixel format of the given target. - target (:obj:`~sdl2.SDL_PixelFormat`, :obj:`~sdl2.SDL_Surface`, - :obj:`~sdl2.ext.SoftwareSprite`): The target pixel format, surface, - or sprite for which the color should be prepared. + target (:obj:`SDL_PixelFormat`, :obj:`SDL_Surface`, :obj:`SoftwareSprite`): The + target pixel format, surface, or sprite for which the color should be + prepared. Returns: int: An integer approximating the given color in the target's pixel - format. + format. -""" + """ color = convert_to_color(color) pformat = None # Software surfaces @@ -58,7 +47,7 @@ def prepare_color(color, target): def fill(target, color, area=None): """Fills one or more rectangular areas on a surface with a given color. - Fill areas can be specified as 4-item (x, y, w, h) tuples, + Fill areas can be specified as 4-item ``(x, y, w, h)`` tuples, :obj:`~sdl2.rect.SDL_Rect` objects, or a list containing multiple areas to fill in either format. If no area is provided, the entire target will be filled with the provided color. @@ -121,9 +110,9 @@ def line(target, color, dline, width=1): target (:obj:`~sdl2.SDL_Surface`, :obj:`~sdl2.ext.SoftwareSprite`): The target surface or sprite to modify. color (:obj:`sdl2.ext.Color`): The color with which to draw lines. - dline (tuple, list): The (x1, y1, x2, y2) integer coordinates of a line - to draw, or a list of multiple sets of (x1, y1, x2, y2) coordinates - for multiple lines. + dline (tuple, list): The ``(x1, y1, x2, y2)`` integer coordinates of a + line to draw, or a list of multiple sets of ``(x1, y1, x2, y2)`` + coordinates for multiple lines. width (int, optional): The width of the line(s) in pixels. Defaults to 1 if not specified. @@ -155,7 +144,7 @@ def line(target, color, dline, width=1): top, bottom = clip_rect.y, clip_rect.y + clip_rect.h - 1 if bpp == 3: - raise UnsupportedError(line, "24bpp are currently not supported") + raise UnsupportedError("24bpp surfaces are not currently supported.") if bpp == 2: pxbuf = ctypes.cast(rtarget.pixels, ctypes.POINTER(ctypes.c_uint16)) elif bpp == 4: @@ -182,7 +171,7 @@ def line(target, color, dline, width=1): fillrect(rtarget, varea, color) continue if width != 1: - raise UnsupportedError(line, "width > 1 is not supported") + raise ValueError("Diagonal lines must have a width of 1.") if width == 1: # Bresenham x1, y1, x2, y2 = clipline(left, top, right, bottom, x1, y1, x2, y2) diff --git a/sdl2/ext/events.py b/sdl2/ext/events.py index 43661b73..a055419c 100644 --- a/sdl2/ext/events.py +++ b/sdl2/ext/events.py @@ -79,8 +79,7 @@ class MPEventHandler(EventHandler): """ def __init__(self, sender, maxprocs=None): if not _HASMP: - raise UnsupportedError(MPEventHandler, - "no multiprocessing support found") + raise UnsupportedError("no multiprocessing support found") super(MPEventHandler, self).__init__(sender) self.maxprocs = maxprocs diff --git a/sdl2/ext/font.py b/sdl2/ext/font.py deleted file mode 100644 index bd85b2f3..00000000 --- a/sdl2/ext/font.py +++ /dev/null @@ -1,355 +0,0 @@ -"""Font and text rendering routines.""" -import os -from .. import surface, rect, pixels -from .common import SDLError -from .compat import * -from .sprite import SoftwareSprite -from .color import Color, convert_to_color -from .draw import _get_target_surface, prepare_color - -_HASSDLTTF = True -try: - from .. import sdlttf -except ImportError: - _HASSDLTTF = False - - -__all__ = ["BitmapFont", "FontManager"] - - -class BitmapFont(object): - """A bitmap graphics to character mapping. - - The BitmapFont class uses an image surface to find and render font - character glyphs for text. It requires a mapping table, which - denotes the characters available on the image. - - The mapping table is a list of strings, where each string reflects a - 'line' of characters on the image. Each character within each line - has the same size as specified by the size argument. - - A typical mapping table might look like - - [ '0123456789', - 'ABCDEFGHIJ', - 'KLMNOPQRST', - 'UVWXYZ ', - 'abcdefghij', - 'klmnopqrst', - 'uvwxyz ', - ',;.:!?+-()' ] - """ - - DEFAULTMAP = ["0123456789", - "ABCDEFGHIJ", - "KLMNOPQRST", - "UVWXYZ ", - "abcdefghij", - "klmnopqrst", - "uvwxyz ", - ",;.:!?+-()" - ] - - - def __init__(self, imgsurface, size, mapping=None): - """Creates a new BitmapFont instance from the passed image. - - Each character is expected to be of the same size (a 2-value tuple - denoting the width and height) and to be in order of the passed - mapping. - """ - if mapping is None: - self.mapping = list(BitmapFont.DEFAULTMAP) - else: - self.mapping = mapping - self.offsets = {} - if isinstance(imgsurface, SoftwareSprite): - self.surface = imgsurface.surface - self._sprite = imgsurface # prevent GC on the Sprite - elif isinstance(imgsurface, surface.SDL_Surface): - self.surface = imgsurface - elif "SDL_Surface" in str(type(imgsurface)): - self.surface = imgsurface.contents - else: - raise TypeError("imgsurface must be a Sprite or SDL_Surface") - self.size = size[0], size[1] - self._calculate_offsets() - - def _calculate_offsets(self): - """Calculates the internal character offsets for each line.""" - self.offsets = {} - offsets = self.offsets - x, y = 0, 0 - w, h = self.size - for line in self.mapping: - x = 0 - for c in line: - offsets[c] = rect.SDL_Rect(x, y, w, h) - x += w - y += h - - def _validate_chars(self, text): - e = "The character '{0}' does not exist within the current font mapping" - for ch in text: - if ch not in self.offsets.keys(): - raise ValueError(e.format(ch)) - - def _render_text(self, target, fontsf, lines, offset=(0, 0)): - w, h = self.size - dstr = rect.SDL_Rect(0, 0, 0, 0) - y = offset[1] - for line in lines: - dstr.y = y - x = offset[0] - for c in line: - dstr.x = x - surface.SDL_BlitSurface(fontsf, self.offsets[c], target, dstr) - x += w - y += h - return (x, y) - - def render(self, text, bpp=None): - """Renders the passed text on a new Sprite and returns it.""" - w, h = self.size - self._validate_chars(text) - lines = text.split(os.linesep) - - tw, th = 0, 0 - for line in lines: - tw = max(tw, sum([w for c in line])) - th += h - if bpp is None: - bpp = self.surface.format.contents.BitsPerPixel - sf = surface.SDL_CreateRGBSurface(0, tw, th, bpp, 0, 0, 0, 0) - if not sf: - raise SDLError() - imgsurface = SoftwareSprite(sf.contents, False) - - self._render_text(imgsurface.surface, self.surface, lines) - return imgsurface - - def render_on(self, imgsurface, text, offset=(0, 0)): - """Renders a text on the passed sprite, starting at a specific - offset. - - The top-left start position of the text will be the passed offset and - 4-value tuple with the changed area will be returned. - """ - w, h = self.size - target = _get_target_surface(imgsurface) - self._validate_chars(text) - lines = text.split(os.linesep) - - x, y = self._render_text(target, self.surface, lines, offset) - return (offset[0], offset[1], x + w, y + h) - - def contains(self, c): - """Checks, whether a certain character exists in the font.""" - return c == ' ' or c in self.offsets - - def can_render(self, text): - """Checks, whether all characters in the passed text can be rendered. - """ - lines = text.split(os.linesep) - for line in lines: - for c in line: - if c != ' ' and c not in self.offsets: - return False - return True - - -class FontManager(object): - """Manage fonts and rendering of text.""" - def __init__(self, font_path, alias=None, size=16, - color=Color(255, 255, 255), bg_color=Color(0, 0, 0), index=0): - """Initialize the FontManager - - One font path must be given to initialize the FontManager. The - default_font will be set to this font. color and bg_color - will give the FontManager a default color. size is the default - font size in pixels. - """ - if not _HASSDLTTF: - raise UnsupportedError(FontManager, - "FontManager requires sdlttf support") - if sdlttf.TTF_WasInit() == 0 and sdlttf.TTF_Init() != 0: - raise SDLError() - self.fonts = {} # fonts = {alias: {size:font_ptr}} - self.aliases = {} # aliases = {alias:font_path} - self._textcolor = pixels.SDL_Color(0, 0, 0) - self._bgcolor = pixels.SDL_Color(255, 255, 255) - self.color = color - self.bg_color = bg_color - self.size = size - self._default_font = self.add(font_path, alias, size, index) - - def __del__(self): - """Close all opened fonts.""" - self.close() - - def close(self): - """Close all opened fonts.""" - for alias, fonts in self.fonts.items(): - for size, font in fonts.items(): - if font: - sdlttf.TTF_CloseFont(font) - self.fonts = {} - self.aliases = {} - - def add(self, font_path, alias=None, size=None, index=0): - """Add a font to the Font Manager. - - alias is by default the font name. But another name can be - passed. Returns the font pointer stored in self.fonts. - """ - size = size or self.size - if alias is None: - # If no alias given, take the font name as alias - basename = os.path.basename(font_path) - alias = os.path.splitext(basename)[0] - if alias in self.fonts: - if size in self.fonts[alias] and self.fonts[alias]: - # font with selected size already opened - return - else: - self._change_font_size(alias, size) - return - else: - if not os.path.isfile(font_path): - raise IOError("Cannot find %s" % font_path) - - font = self._load_font(font_path, size, index) - self.aliases[alias] = font_path - self.fonts[alias] = {} - self.fonts[alias][size] = font - return font - - def _load_font(self, font_path, size, index=0): - """Helper function to open the font. - - Raises an exception if something went wrong. - """ - if index == 0: - font = sdlttf.TTF_OpenFont(byteify(font_path, "utf-8"), size) - else: - font = sdlttf.TTF_OpenFontIndex(byteify(font_path, "utf-8"), size, - index) - if not font: - raise SDLError(sdlttf.TTF_GetError()) - return font - - def _change_font_size(self, alias, size): - """Loads an already opened font in another size.""" - if alias not in self.fonts: - raise KeyError("Font %s not loaded in FontManager" % alias) - font = self._load_font(self.aliases[alias], size) - self.fonts[alias][size] = font - - @property - def color(self): - """The text color to be used.""" - return Color(self._textcolor.r, self._textcolor.g, self._textcolor.b, - self._textcolor.a) - - @color.setter - def color(self, value): - """The text color to be used.""" - c = convert_to_color(value) - self._textcolor = pixels.SDL_Color(c.r, c.g, c.b, c.a) - - @property - def bg_color(self): - """The background color to be used.""" - return Color(self._bgcolor.r, self._bgcolor.g, self._bgcolor.b, - self._bgcolor.a) - - @bg_color.setter - def bg_color(self, value): - """The background color to be used.""" - c = convert_to_color(value) - self._bgcolor = pixels.SDL_Color(c.r, c.g, c.b, c.a) - - @property - def default_font(self): - """Returns the name of the current default_font.""" - for alias in self.fonts: - for size, font in self.fonts[alias].items(): - if font == self._default_font: - return alias - - @default_font.setter - def default_font(self, value): - """value must be a font alias - - Set the default_font to the given font name alias, - provided it's loaded in the font manager. - """ - alias = value - size = self.size - if alias not in self.fonts: - raise ValueError("Font %s not loaded in FontManager" % alias) - # Check if size is already loaded, otherwise do it. - if size not in self.fonts[alias]: - self._change_font_size(alias, size) - size = list(self.fonts[alias].keys())[0] - self._default_font = self.fonts[alias][size] - - def render(self, text, alias=None, size=None, width=None, color=None, - bg_color=None, **kwargs): - """Renders text to a surface. - - This method uses the font designated by the alias or the - default_font. A size can be passed even if the font was not - loaded with this size. A width can be given for line wrapping. - If no bg_color or color are given, it will default to the - FontManager's bg_color and color. - """ - alias = alias or self.default_font - size = size or self.size - if bg_color is None: - bg_color = self._bgcolor - elif not isinstance(bg_color, pixels.SDL_Color): - c = convert_to_color(bg_color) - bg_color = pixels.SDL_Color(c.r, c.g, c.b, c.a) - if color is None: - color = self._textcolor - elif not isinstance(color, pixels.SDL_Color): - c = convert_to_color(color) - color = pixels.SDL_Color(c.r, c.g, c.b, c.a) - if len(self.fonts) == 0: - raise TypeError("There are no fonts selected.") - font = self._default_font - if alias not in self.aliases: - raise KeyError("Font %s not loaded" % font) - elif size not in self.fonts[alias]: - self._change_font_size(alias, size) - font = self.fonts[alias][size] - text = byteify(text, "utf-8") - if width: - fontsf = sdlttf.TTF_RenderUTF8_Blended_Wrapped(font, text, color, - width) - if not fontsf: - raise SDLError(sdlttf.TTF_GetError()) - if bg_color != pixels.SDL_Color(0, 0, 0): - fontsf = fontsf.contents - w, h = fontsf.w, fontsf.h - bpp = fontsf.format.contents.BitsPerPixel - fmt = fontsf.format.contents.format - bgsf = surface.SDL_CreateRGBSurfaceWithFormat(0, w, h, bpp, fmt) - if not bgsf: - surface.SDL_FreeSurface(fontsf) - raise SDLError() - bg_color = prepare_color(bg_color, bgsf.contents) - surface.SDL_FillRect(bgsf, None, bg_color) - surface.SDL_BlitSurface(fontsf, None, bgsf, None) - return bgsf.contents - return fontsf.contents - sf = None - if bg_color == pixels.SDL_Color(0, 0, 0): - sf = sdlttf.TTF_RenderUTF8_Blended(font, text, color) - else: - sf = sdlttf.TTF_RenderUTF8_Shaded(font, text, color, - bg_color) - if not sf: - raise SDLError(sdlttf.TTF_GetError()) - return sf.contents diff --git a/sdl2/ext/image.py b/sdl2/ext/image.py index 49d897d1..1ee811be 100644 --- a/sdl2/ext/image.py +++ b/sdl2/ext/image.py @@ -1,7 +1,9 @@ -"""Image loaders.""" -from .common import SDLError -from .compat import UnsupportedError, byteify -from .. import endian, surface, pixels +import os +from .. import endian, surface, pixels, error +from .common import SDLError, raise_sdl_err +from .compat import UnsupportedError, byteify, stringify +from .resources import _validate_path +from .surface import _get_target_surface _HASPIL = True try: @@ -15,145 +17,312 @@ except ImportError: _HASSDLIMAGE = False -__all__ = ["get_image_formats", "load_image"] +__all__ = [ + "get_image_formats", "load_bmp", "load_img", "save_bmp", + "pillow_to_surface", "load_image" +] + + +_SDL_IMAGE_FLAGS = -1 def get_image_formats(): - """Gets the formats supported in the default installation.""" + # This function is deprecated and gives inaccurate results if not _HASPIL and not _HASSDLIMAGE: return ("bmp", ) return ("bmp", "cur", "gif", "ico", "jpg", "lbm", "pbm", "pcx", "pgm", "png", "pnm", "ppm", "svg", "tga", "tif", "webp", "xcf", "xpm") +def _sdl_image_init(): + global _SDL_IMAGE_FLAGS + if _SDL_IMAGE_FLAGS == -1: + _SDL_IMAGE_FLAGS = sdlimage.IMG_Init( + sdlimage.IMG_INIT_JPG | sdlimage.IMG_INIT_PNG | + sdlimage.IMG_INIT_TIF | sdlimage.IMG_INIT_WEBP + ) + return _SDL_IMAGE_FLAGS + + +def _get_mode_properties(mode): + le = endian.SDL_BYTEORDER == endian.SDL_LIL_ENDIAN + rmask, gmask, bmask, amask = (0, 0, 0, 0) + if mode in ("1", "L", "P"): + # 1 = B/W, 1 bit per byte + # "L" = greyscale, 8-bit + # "P" = palette-based, 8-bit + depth = 8 + elif mode == "RGB": + # RGB: 3x8-bit, 24bpp + depth = 24 + rmask = 0x0000FF if le else 0xFF0000 + gmask = 0x00FF00 + bmask = 0xFF0000 if le else 0x0000FF + elif mode in ("RGBA", "RGBX"): + # RGBX: 4x8-bit, no alpha + # RGBA: 4x8-bit, alpha + depth = 32 + rmask = 0x000000FF if le else 0xFF000000 + gmask = 0x0000FF00 if le else 0x00FF0000 + bmask = 0x00FF0000 if le else 0x0000FF00 + if mode == "RGBA": + amask = 0xFF000000 if le else 0x000000FF + else: + raise TypeError("Cannot convert {0} data to surface.".format(mode)) + return (rmask, gmask, bmask, amask, depth) + + +def load_bmp(path): + """Imports a BMP (bitmap image) file as an SDL surface. + + Because BMP importing and exporting is part of the core SDL2 library, + this function is guaranteed to be available on all platforms and + installations that support PySDL2. + + Args: + path (str): The relative (or absolute) path to the BMP image to import. + + Returns: + :obj:`~sdl2.SDL_Surface`: An SDL surface containing the imported image. + + """ + fullpath, fname = _validate_path(path, "an image") + img_surf = surface.SDL_LoadBMP(byteify(fullpath)) + if not img_surf: + raise_sdl_err("importing '{0}' as a BMP".format(fname)) + + return img_surf.contents + + +def save_bmp(source, path, overwrite=False): + """Exports an SDL surface to a BMP (bitmap image) file. + + Because BMP importing and exporting is part of the core SDL2 library, + this function is guaranteed to be available on all platforms and + installations that support PySDL2. + + Args: + source (:obj:`~sdl2.SDL_Surface`): The surface to save as a BMP file. + path (str): The relative (or absolute) path to which the BMP should be + saved. + overwrite (bool, optional): Whether the image should be overwritten if + a file at that path already exists. Defaults to False. + + """ + fullpath, fname = _validate_path(path, "", write=True) + if os.path.exists(fullpath): + if overwrite: + os.remove(fullpath) + else: + e = "A file already exists at the given path: {0}" + raise RuntimeError(e.format(fullpath)) + surf = _get_target_surface(source, argname="source") + ret = surface.SDL_SaveBMP(surf, byteify(fullpath)) + if ret != 0: + raise_sdl_err("saving '{0}' as a BMP".format(fname)) + + +def load_img(path, as_argb=True): + """Imports an image file as an SDL surface using the **SDL_image** library. + + This function supports a wide range of image formats, including GIF, BMP, + JPEG, PNG, TIFF, and WebP. For a full list, consult the SDL_image + documentation. + + By default, this function also converts the imported surface to 32-bit ARGB + format for consistency across functions and better compatibility with SDL2 + renderers. To disable ARGB conversion, set the ``as_argb`` parameter to + ``False``. + + .. note:: + Because SDL_image is not part of the core SDL2 library, this function + will only work on systems where the SDL_image library is installed. + Additionally, support for PNG, JPEG, TIFF, and WebP in SDL_image is + dynamic and are not guaranteed to be available on all systems. + + Args: + path (str): The relative (or absolute) path to the image to import. + as_argb (bool, optional): Whether the obtained surface should be + converted to 32-bit ARGB pixel format or left as-is. Defaults to + ``True`` (convert to ARGB). + + Returns: + :obj:`~sdl2.SDL_Surface`: An SDL surface containing the imported image. + + """ + if not _HASSDLIMAGE: + err = "'{0}' requires the SDL_image library, which could not be found." + raise RuntimeError(err.format("load_img")) + + # Import the image file using the generic SDL_Image loader + fullpath, fname = _validate_path(path, "an image") + _sdl_image_init() + img_surf = sdlimage.IMG_Load(byteify(fullpath)) + if not img_surf: + raise_sdl_err("importing '{0}' using SDL_image".format(fname)) + + # Determine whether to use 32-bit ARGB or original pixel format + ARGB32 = pixels.SDL_PIXELFORMAT_ARGB8888 + out_fmt = img_surf.contents.format + if as_argb and out_fmt.contents.format != ARGB32: + out_fmt = pixels.SDL_AllocFormat(ARGB32) + surfcopy = surface.SDL_ConvertSurface(img_surf, out_fmt, 0) + surface.SDL_FreeSurface(img_surf) + img_surf = surfcopy + if not img_surf: + raise_sdl_err("converting '{0}' to ARGB format".format(fname)) + + return img_surf.contents + + +def pillow_to_surface(img, as_argb=True): + """Converts a :obj:`PIL.Image.Image` object to an SDL surface. + + This function returns a copy of the original object's pixel data, meaning + that the original Image can be modified or deleted without affecting the + returned surface (and vice versa). + + By default, this function also converts the surface to 32-bit ARGB format + for consistency across functions and better compatibility with SDL2 + renderers. To disable ARGB conversion, set the ``as_argb`` parameter to + ``False``. + + Args: + img (:obj:`PIL.Image.Image`): The Image object to convert to an SDL + surface. + as_argb (bool, optional): Whether the obtained surface should be + converted to 32-bit ARGB pixel format or left as-is. Defaults to + ``True`` (convert to ARGB). + + Returns: + :obj:`~sdl2.SDL_Surface`: An SDL surface copy of the PIL image. + + """ + if not (hasattr(img, "mode") and hasattr(img, "size")): + raise TypeError("'img' must be a valid PIL Image.") + + # Determine correct properties for new surface from PIL data + mode = img.mode + width, height = img.size + rmask, gmask, bmask, amask, depth = _get_mode_properties(mode) + pitch = width * int(depth / 8) + + # Get PIL pixel bytes and cast them to an SDL surface + pxbuf = img.tobytes() + imgsurface = surface.SDL_CreateRGBSurfaceFrom( + pxbuf, width, height, depth, pitch, rmask, gmask, bmask, amask + ) + if not imgsurface: + raise_sdl_err("creating a surface from a PIL Image") + imgsurface = imgsurface.contents + + # Retrieve the palette for the image (if any) + palette = [] + if mode == "P": + palette = img.getpalette() + elif mode in ("1", "L"): + for i in range(256): + palette += [i, i, i] + + if len(palette): + # Convert the Pillow palette to an SDL palette + num_colors = len(palette) // 3 + sdlpalette = pixels.SDL_AllocPalette(num_colors) + if not sdlpalette: + raise_sdl_err("initializing the palette for the SDL surface") + for idx in range(num_colors): + start, end = (idx * 3, idx * 3 + 3) + r, g, b = palette[start:end] + sdlpalette.contents.colors[idx] = pixels.SDL_Color(r, g, b) + + # Apply the converted palette to the surface + ret = surface.SDL_SetSurfacePalette(imgsurface, sdlpalette) + pixels.SDL_FreePalette(sdlpalette) + if ret != 0: + raise_sdl_err("converting the palette from the PIL Image") + + # If the image has a single transparent palette index, set that index as + # the color key to make blitting correct. + k = "transparency" + if k in img.info and isinstance(img.info[k], int): + surface.SDL_SetColorKey(imgsurface, True, img.info[k]) + + # Determine whether to use 32-bit ARGB or original pixel format + out_fmt = imgsurface.format + if as_argb: + out_fmt = pixels.SDL_AllocFormat(pixels.SDL_PIXELFORMAT_ARGB8888) + + # Create a new surface from the converted data for memory safety + surfcopy = surface.SDL_ConvertSurface(imgsurface, out_fmt, 0) + surface.SDL_FreeSurface(imgsurface) + if not surfcopy: + raise_sdl_err("copying the PIL Image data to a new surface") + + return surfcopy.contents + + def load_image(fname, enforce=None): - """Creates a SDL_Surface from an image file. + """**[Deprecated]** Imports an image file as an SDL surface. + + This function uses either the SDL_image library or the Pillow Python package + for importing images, using SDL2's built-in BMP loader as a fall-back if + neither are available. - This function makes use of the Python Imaging Library, if it is available - on the target execution environment. The function will try to load the - file via sdl2 first. If the file could not be loaded, it will try - to load it via sdl2.sdlimage and PIL. + .. warning:: + Due to a long-standing bug, the resulting image surfaces can have + different pixel formats depending on which backend was used, making + behavior unpredictable across different systems. As such this function + is deprecated, and is only maintained to avoid breaking existing code. + For new projects, the :func:`load_bmp`, :func:`load_img`, and/or + :func:`pillow_to_surface` functions should be used instead. - You can force the function to use only one of them, by passing the enforce - as either "PIL" or "SDL". + Args: + fname (str): The relative (or absolute) path to the image to import. + enforce (str, optional): A string indicating the specific backend to + use for loading images. Can be either "PIL" for Pillow-only, "SDL" + for SDL2 and SDL_image only, or ``None`` for no enforced backend. + Defaults to ``None``. + + Returns: + :obj:`~sdl2.SDL_Surface`: An SDL surface containing the imported image. - Note: This will call sdl2.sdlimage.init() implicitly with the default - arguments, if the module is available and if sdl2.SDL_LoadBMP() failed to - load the image. """ if enforce is not None and enforce not in ("PIL", "SDL"): raise ValueError("enforce must be either 'PIL' or 'SDL', if set") - if fname is None: + elif enforce == "PIL" and not _HASPIL: + raise UnsupportedError("cannot use PIL (not found)") + if fname is None or not hasattr(fname, "upper"): raise ValueError("fname must be a string") - name = fname - if hasattr(fname, 'encode'): - name = byteify(fname, "utf-8") + name = byteify(fname) + imgsurface = None + err = "Unable to import '{0}'".format(fname) - if not _HASPIL and not _HASSDLIMAGE: + # Try importing image as a BMP if other decoders aren't available + if (enforce == "SDL" or not _HASPIL) and not _HASSDLIMAGE: imgsurface = surface.SDL_LoadBMP(name) if not imgsurface: - raise UnsupportedError(load_image, - "cannot use PIL or SDL for image loading") - return imgsurface.contents - if enforce == "PIL" and not _HASPIL: - raise UnsupportedError(load_image, "cannot use PIL (not found)") - if enforce == "SDL" and not _HASSDLIMAGE: - imgsurface = surface.SDL_LoadBMP(name) - if not imgsurface: - raise UnsupportedError(load_image, - "cannot use SDL_image (not found)") - return imgsurface.contents + error.SDL_ClearError() + err += " as a BMP (must have SDL_image or Pillow to support " + err += "other formats)" + raise RuntimeError(err) + else: + return imgsurface.contents - imgsurface = None + # Try imporing the image using SDL_image if enforce != "PIL" and _HASSDLIMAGE: - sdlimage.IMG_Init(sdlimage.IMG_INIT_JPG | sdlimage.IMG_INIT_PNG | - sdlimage.IMG_INIT_TIF | sdlimage.IMG_INIT_WEBP) + _sdl_image_init() imgsurface = sdlimage.IMG_Load(name) if not imgsurface: # An error occured - if we do not try PIL, break out now if not _HASPIL or enforce == "SDL": - raise SDLError(sdlimage.IMG_GetError()) + err += " using SDL_image: " + stringify(error.SDL_GetError()) + error.SDL_ClearError() + raise RuntimeError(err) else: - imgsurface = imgsurface.contents + return imgsurface.contents - if enforce != "SDL" and _HASPIL and not imgsurface: + # Try importing the image using Pillow and converting to an SDL surface + if enforce != "SDL" and _HASPIL: image = Image.open(fname) - mode = image.mode - width, height = image.size - rmask = gmask = bmask = amask = 0 - if mode in ("1", "L", "P"): - # 1 = B/W, 1 bit per byte - # "L" = greyscale, 8-bit - # "P" = palette-based, 8-bit - pitch = width - depth = 8 - elif mode == "RGB": - # 3x8-bit, 24bpp - if endian.SDL_BYTEORDER == endian.SDL_LIL_ENDIAN: - rmask = 0x0000FF - gmask = 0x00FF00 - bmask = 0xFF0000 - else: - rmask = 0xFF0000 - gmask = 0x00FF00 - bmask = 0x0000FF - depth = 24 - pitch = width * 3 - elif mode in ("RGBA", "RGBX"): - # RGBX: 4x8-bit, no alpha - # RGBA: 4x8-bit, alpha - if endian.SDL_BYTEORDER == endian.SDL_LIL_ENDIAN: - rmask = 0x000000FF - gmask = 0x0000FF00 - bmask = 0x00FF0000 - if mode == "RGBA": - amask = 0xFF000000 - else: - rmask = 0xFF000000 - gmask = 0x00FF0000 - bmask = 0x0000FF00 - if mode == "RGBA": - amask = 0x000000FF - depth = 32 - pitch = width * 4 - else: - # We do not support CMYK or YCbCr for now - raise TypeError("unsupported image format") - - pxbuf = image.tobytes() - imgsurface = surface.SDL_CreateRGBSurfaceFrom(pxbuf, width, height, - depth, pitch, rmask, - gmask, bmask, amask) - if not imgsurface: - raise SDLError() - imgsurface = imgsurface.contents - # the pixel buffer must not be freed for the lifetime of the surface - imgsurface._pxbuf = pxbuf - - if mode == "P": - # Create a SDL_Palette for the SDL_Surface - def _chunk(seq, size): - for x in range(0, len(seq), size): - yield seq[x:x + size] - - rgbcolors = image.getpalette() - sdlpalette = pixels.SDL_AllocPalette(len(rgbcolors) // 3) - if not sdlpalette: - raise SDLError() - SDL_Color = pixels.SDL_Color - for idx, (r, g, b) in enumerate(_chunk(rgbcolors, 3)): - sdlpalette.contents.colors[idx] = SDL_Color(r, g, b) - ret = surface.SDL_SetSurfacePalette(imgsurface, sdlpalette) - # This will decrease the refcount on the palette, so it gets - # freed properly on releasing the SDL_Surface. - pixels.SDL_FreePalette(sdlpalette) - if ret != 0: - raise SDLError() - - # If the image has a single transparent palette index, set - # that index as the color key to make blitting correct. - if 'transparency' in image.info and isinstance(image.info['transparency'], int): - surface.SDL_SetColorKey(imgsurface, True, image.info['transparency']) - - return imgsurface + return pillow_to_surface(image, as_argb=False) diff --git a/sdl2/ext/msgbox.py b/sdl2/ext/msgbox.py new file mode 100644 index 00000000..74657b69 --- /dev/null +++ b/sdl2/ext/msgbox.py @@ -0,0 +1,237 @@ +from ctypes import byref, c_int +from .color import Color +from .compat import isiterable, utf8 +from .common import SDLError +from .window import Window +from .. import dll, error, SDL_PumpEvents, SDL_Window +from .. import messagebox as mb + +__all__ = [ + "MessageBoxTheme", "MessageBox", "show_messagebox", "show_alert" +] + +class MessageBoxTheme(object): + """Initializes a color scheme for use with :obj:`MessageBox` objects. + + This is used to define the background, text, and various button colors + to use when presenting dialog boxes to users. All colors must be defined + as either :obj:`sdl2.ext.Color` objects or 8-bit ``(r, g, b)`` tuples. + + .. note:: + SDL2 only supports MessageBox themes on a few platforms, including + Linux/BSD (if using X11) and Haiku. MessageBox themes will have no effect + on Windows, macOS, or Linux if using Wayland. + + Args: + bg (:obj:`~sdl2.ext.Color`, tuple, optional): The color to use for the + background of the dialog box. Defaults to ``(56, 54, 53)``. + text (:obj:`~sdl2.ext.Color`, tuple, optional): The color to use for the + text of the dialog box. Defaults to ``(209, 207, 205)``. + btn (:obj:`~sdl2.ext.Color`, tuple, optional): The color to use for the + backgrounds of buttons. Defaults to ``(140, 135, 129)``. + btn_border (:obj:`~sdl2.ext.Color`, tuple, optional): The color to use + for the borders of buttons. Defaults to ``(105, 102, 99)``. + btn_selected (:obj:`~sdl2.ext.Color`, tuple, optional): The color to use + for selected buttons. Defaults to ``(205, 202, 53)``. + + """ + def __init__( + self, bg=None, text=None, btn=None, btn_border=None, btn_selected=None + ): + # NOTE: Default colors taken from SDL_x11messagebox.c + self._theme = [ + (56, 54, 53), # Background color + (209, 207, 205), # Text color + (140, 135, 129), # Button border color + (105, 102, 99), # Button background color + (205, 202, 53) # Selected button color + ] + # Update default theme colors based on provided values + elements = [bg, text, btn_border, btn, btn_selected] + for i in range(len(elements)): + if elements[i] is not None: + self._theme[i] = self._validate_color(elements[i]) + + def _validate_color(self, col): + if not isinstance(col, Color): + if not isiterable(col) or len(col) != 3: + e = "MessageBox colors must be specified as (r, g, b) tuples." + raise TypeError(e) + for val in col: + if int(val) != float(val): + e = "All RGB values must be integers between 0 and 255." + raise ValueError(e) + col = Color(col[0], col[1], col[2]) + return (col.r, col.g, col.b) + + def _get_theme(self): + sdl_colors = [] + for col in self._theme: + sdl_colors.append(mb.SDL_MessageBoxColor(*col)) + col_array = (mb.SDL_MessageBoxColor * 5)(*sdl_colors) + return mb.SDL_MessageBoxColorScheme(col_array) + + +class MessageBox(object): + """Creates a prototype for a dialog box that can be presented to the user. + + The `MessageBox` class is for designing a dialog box in the style of the + system's window manager, containing a title, a message to present, and + one or more response buttons. + + Args: + title (str): The title to use for the dialog box. All UTF-8 characters + are supported. + msg (str): The main body of text to display in the dialog box. All UTF-8 + characters are supported. + buttons (list): A list of strings, containing the labels of the buttons + to place at the bottom of the dialog box (e.g. ``["No", "Yes"]``). + Buttons will be placed in left-to-right order. + default (str, optional): The label of the button to highlight as the + default option (e.g. ``"Yes"``). Must match one of the labels in + ``buttons``. This option will be accepted if the Return/Enter key + is pressed on the keyboard. + msgtype (str, optional): The type of dialog box to create, if supported + by the system. On most window managers, this changes the icon used + in the dialog box. Must be one of 'error', 'warning', or 'info', or + None (the default). + theme (:obj:`MessageBoxTheme`, optional): The color scheme to use for + the dialog box, if supported by the window manager. Defaults to the + system default theme. + + """ + def __init__(self, title, msg, buttons, default=None, msgtype=None, theme=None): + self._title = utf8(title).encode('utf-8') + self._text = utf8(msg).encode('utf-8') + self._validate_buttons(buttons) + self._buttons = buttons + self._sdlbuttons = self._init_buttons(buttons, default) + self._type = self._set_msgtype(msgtype) if msgtype else 0 + self._theme = theme._get_theme() if theme else None + + def _set_msgtype(self, msgtype): + _flagmap = { + 'error': mb.SDL_MESSAGEBOX_ERROR, + 'warning': mb.SDL_MESSAGEBOX_WARNING, + 'info': mb.SDL_MESSAGEBOX_INFORMATION, + } + if msgtype.lower() not in _flagmap.keys(): + raise ValueError( + "MessageBox type must be 'error', 'warning', 'info', or None." + ) + return _flagmap[msgtype] + + def _validate_buttons(self, buttons): + if not isiterable(buttons): + raise TypeError("Buttons must be provided as a list.") + elif len(buttons) == 0: + raise ValueError("MessageBox must have at least one button.") + + def _init_buttons(self, buttons, default): + default_flag = mb.SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT + buttonset = [] + for i in range(len(buttons)): + b = mb.SDL_MessageBoxButtonData( + flags = (default_flag if buttons[i] == default else 0), + buttonid = i, + text = utf8(buttons[i]).encode('utf-8'), + ) + buttonset.append(b) + return (mb.SDL_MessageBoxButtonData * len(buttons))(*buttonset) + + def _get_window_pointer(self, win): + if isinstance(win, Window): + win = win.window + if isinstance(win, SDL_Window): + win = dll.get_pointer(win) + if hasattr(win, "contents") and isinstance(win.contents, SDL_Window): + return win + else: + e = "'window' must be a Window or SDL_Window object (got {0})" + raise ValueError(e.format(str(type(win)))) + + def _get_msgbox(self, window=None): + if window: + window = self._get_window_pointer(window) + return mb.SDL_MessageBoxData( + flags = self._type | mb.SDL_MESSAGEBOX_BUTTONS_RIGHT_TO_LEFT, + window = window, + title = self._title, + message = self._text, + numbuttons = len(self._buttons), + buttons = self._sdlbuttons, + colorScheme = dll.get_pointer(self._theme) if self._theme else None, + ) + + +def show_messagebox(msgbox, window=None): + """Displays a dialog box to the user and waits for a response. + + By default message boxes are presented independently of any window, but + they can optionally be attached explicitly to a specific SDL window. This + prevents that window from regaining focus until a response to the dialog + box is made. + + Args: + msgbox (:obj:`~sdl2.ext.MessageBox`): The dialog box to display + on-screen. + window (:obj:`~sdl2.SDL_Window`, :obj:`~sdl2.ext.Window`, optional): The + window to associate with the dialog box. Defaults to None. + + Returns: + str: The label of the button selected by the user. + + """ + resp = c_int(-1) + ret = mb.SDL_ShowMessageBox( + msgbox._get_msgbox(window), + byref(resp) + ) + SDL_PumpEvents() + if ret == 0: + return msgbox._buttons[resp.value] + else: + errmsg = error.SDL_GetError().decode('utf-8') + error.SDL_ClearError() + e = "Error encountered displaying message box" + if len(errmsg): + e += ": {0}".format(errmsg) + raise SDLError(e) + + +def show_alert(title, msg, msgtype=None, window=None): + """Displays a simple alert to the user and waits for a response. + + This function is a simplified version of :func:`show_messagebox` for cases + where only one response button ("OK") is needed and a custom color scheme + is not necessary. + + By default message boxes are presented independently of any window, but + they can optionally be attached explicitly to a specific SDL window. This + prevents that window from regaining focus until a response to the dialog + box is made. + + Args: + msgbox (:obj:`~sdl2.ext.MessageBox`): The dialog box to display + on-screen. + window (:obj:`~sdl2.SDL_Window`, :obj:`~sdl2.ext.Window`, optional): The + window to associate with the dialog box. Defaults to ``None``. + + """ + box = MessageBox(title, msg, ["OK"], msgtype=msgtype) + if window: + window = box._get_window_pointer(window) + ret = mb.SDL_ShowSimpleMessageBox( + box._type, + box._title, + box._text, + window + ) + SDL_PumpEvents() + if ret != 0: + errmsg = error.SDL_GetError().decode('utf-8') + error.SDL_ClearError() + e = "Error encountered displaying message box" + if len(errmsg): + e += ": {0}".format(errmsg) + raise SDLError(e) diff --git a/sdl2/ext/pixelaccess.py b/sdl2/ext/pixelaccess.py index 581d4abf..02c0b6ca 100644 --- a/sdl2/ext/pixelaccess.py +++ b/sdl2/ext/pixelaccess.py @@ -1,36 +1,54 @@ -"""Pixel-wise access routines.""" import ctypes from .compat import UnsupportedError, experimental from .array import MemoryView from ..surface import SDL_MUSTLOCK, SDL_LockSurface, SDL_UnlockSurface, \ SDL_Surface from ..stdinc import Uint8 +from .draw import prepare_color from .sprite import SoftwareSprite -from .draw import _get_target_surface, prepare_color +from .surface import _get_target_surface +try: + import numpy + _HASNUMPY = True +except ImportError: + _HASNUMPY = False -__all__ = ["PixelView", "pixels2d", "pixels3d"] +__all__ = [ + "PixelView", "SurfaceArray", "pixels2d", "pixels3d", "surface_to_ndarray" +] class PixelView(MemoryView): - """2D memory view for Sprite and SDL_Surface pixel access. + """A 2D memory view for reading and writing SDL surface pixels. + + This class uses a ``view[y][x]`` layout, with the y-axis as the first + dimension and the x-axis as the second. ``PixelView`` objects currently do + not support array slicing, but support negative indexing as of + PySDL2 0.9.10. - The PixelView uses a y/x-layout. Accessing view[N] will operate on the - Nth row of the underlying surface. To access a specific column within - that row, view[N][C] has to be used. + If the source surface is RLE-accelerated, it will be locked automatically + when the view is created and you will need to re-lock the surface using + :func:`SDL_UnlockSurface` once you are done with the view. + + .. warning:: + The source surface should not be freed or deleted until the view is no + longer needed. Accessing the view for a freed surface will likely cause + Python to hard-crash. + + .. note:: + This class is implemented on top of the :class:`~sdl2.ext.MemoryView` + class. As such, it makes heavy use of recursion to access rows and + will generally be much slower than the :mod:`numpy`-based + :func:`~sdl2.ext.pixels2d` and :func:`~sdl2.ext.pixels3d` functions. + + Args: + source (:obj:`~sdl2.SDL_Surface`, :obj:`~sdl2.ext.SoftwareSprite`): The + SDL surface to access with the view. - NOTE: The PixelView is implemented on top of the MemoryView class. As such - it makes heavy use of recursion to access rows and columns and can be - considered as slow in contrast to optimised ndim-array solutions such as - numpy. """ + # TODO: Add support for negative indexing and/or out-of-bounds errors? def __init__(self, source): - """Creates a new PixelView from a Sprite or SDL_Surface. - - If necessary, the surface will be locked for accessing its pixel data. - The lock will be removed once the PixelView is garbage-collected or - deleted. - """ if isinstance(source, SoftwareSprite): self._surface = source.surface # keep a reference, so the Sprite's not GC'd @@ -42,11 +60,15 @@ def __init__(self, source): else: raise TypeError("source must be a Sprite or SDL_Surface") + itemsize = self._surface.format.contents.BytesPerPixel + if itemsize == 3: + e = "Cannot open a 3 bytes-per-pixel surface using a PixelView." + raise RuntimeError(e) + if SDL_MUSTLOCK(self._surface): SDL_LockSurface(self._surface) pxbuf = ctypes.cast(self._surface.pixels, ctypes.POINTER(Uint8)) - itemsize = self._surface.format.contents.BytesPerPixel strides = (self._surface.h, self._surface.w) srcsize = self._surface.h * self._surface.pitch super(PixelView, self).__init__(pxbuf, itemsize, strides, @@ -63,9 +85,6 @@ def _getitem(self, start, end): casttype = ctypes.c_ubyte if self.itemsize == 2: casttype = ctypes.c_ushort - elif self.itemsize == 3: - # TODO - raise NotImplementedError("unsupported bpp") elif self.itemsize == 4: casttype = ctypes.c_uint return ctypes.cast(src, ctypes.POINTER(casttype)).contents.value @@ -76,99 +95,228 @@ def _setitem(self, start, end, value): target = ctypes.cast(self.source, ctypes.POINTER(ctypes.c_ubyte)) elif self.itemsize == 2: target = ctypes.cast(self.source, ctypes.POINTER(ctypes.c_ushort)) - elif self.itemsize == 3: - # TODO - raise NotImplementedError("unsupported bpp") elif self.itemsize == 4: target = ctypes.cast(self.source, ctypes.POINTER(ctypes.c_uint)) value = prepare_color(value, self._surface) target[start // self.itemsize] = value - def __del__(self): - if self._surface: - if SDL_MUSTLOCK(self._surface): - SDL_UnlockSurface(self._surface) -_HASNUMPY = True -try: - import numpy +def _ndarray_prep(source, funcname, ndim): + # Internal function for preparing SDL_Surfaces for casting to ndarrays + if not _HASNUMPY: + err = "'{0}' requires Numpy, which could not be found." + raise UnsupportedError(err.format(funcname)) + + # Get SDL surface and extract required attributes + psurface = _get_target_surface(source, argname="source") + sz = psurface.h * psurface.pitch + bpp = psurface.format.contents.BytesPerPixel + if bpp < 1 or bpp > 4: + err = "The bpp of the source surface must be between 1 and 4, inclusive" + raise ValueError(err + " (got {0}).".format(bpp)) + elif bpp == 3 and ndim == 2: + err = "Surfaces with 3 bytes-per-pixel cannot be cast as 2D arrays." + raise RuntimeError(err) - class SurfaceArray(numpy.ndarray): - """Wrapper class around numpy.ndarray. + # Handle 2D and 3D arrays differently where needed + if ndim == 2: + dtypes = { + 1: numpy.uint8, + 2: numpy.uint16, + 4: numpy.uint32 + } + strides = (psurface.pitch, bpp) + shape = psurface.h, psurface.w + dtype = dtypes[bpp] + else: + strides = (psurface.pitch, bpp, 1) + shape = psurface.h, psurface.w, bpp + dtype = numpy.uint8 - Used to keep track of the original source object for pixels2d() - and pixels3d() to avoid the deletion of the source object. - """ - def __new__(cls, shape, dtype=float, buffer_=None, offset=0, - strides=None, order=None, source=None, surface=None): - sfarray = numpy.ndarray.__new__(cls, shape, dtype, buffer_, - offset, strides, order) - sfarray._source = source - sfarray._surface = surface - return sfarray + return (psurface, sz, shape, dtype, strides) - def __array_finalize__(self, sfarray): - if sfarray is None: - return - self._source = getattr(sfarray, '_source', None) - self._surface = getattr(sfarray, '_surface', None) - def __del__(self): - if self._surface: - if SDL_MUSTLOCK(self._surface): - SDL_UnlockSurface(self._surface) +def pixels2d(source, transpose=True): + """Creates a 2D Numpy array view for a given SDL surface. -except ImportError: - _HASNUMPY = False + This function casts the surface pixels to a 2D Numpy array view, providing + read and write access to the underlying surface. If the source surface is + RLE-accelerated, it will be locked automatically when the view is created + and you will need to re-lock the surface using :func:`SDL_UnlockSurface` + once you are done with the array. + By default, the array is returned in ``arr[x][y]`` format with the x-axis + as the first dimension, contrary to PIL and PyOpenGL convention. To obtain + an ``arr[y][x]`` array, set the ``transpose`` argument to ``False``. -@experimental -def pixels2d(source, transpose=True): - """Creates a 2D pixel array from the passed source.""" - if not _HASNUMPY: - raise UnsupportedError(pixels2d, "numpy module could not be loaded") - - psurface = _get_target_surface(source, argname="source") - bpp = psurface.format.contents.BytesPerPixel - if bpp < 1 or bpp > 4: - raise ValueError("unsupported bpp") - strides = (psurface.pitch, bpp) - sz = psurface.h * psurface.pitch - shape = psurface.h, psurface.w # surface.pitch // bpp - - dtypes = { - 1: numpy.uint8, - 2: numpy.uint16, - 3: numpy.uint32, - 4: numpy.uint32 - } - - if SDL_MUSTLOCK(psurface): - SDL_LockSurface(psurface) - pxbuf = ctypes.cast(psurface.pixels, ctypes.POINTER(ctypes.c_ubyte * sz)) - arr = SurfaceArray(shape, dtypes[bpp], pxbuf.contents, 0, strides, "C", - source, psurface) + .. warning:: + The source surface should not be freed or deleted until the array is no + longer needed. Accessing the array for a freed surface will likely cause + Python to hard-crash. + + .. note:: + This function requires Numpy to be installed in the current Python + environment. + + Args: + source (:obj:`~sdl2.SDL_Surface`, :obj:`~sdl2.ext.SoftwareSprite`): The + SDL surface to cast to a numpy array. + transpose (bool, optional): Whether the output array should be + transposed to have ``arr[x][y]`` axes instead of ``arr[y][x]`` axes. + Defaults to ``True``. + + Returns: + :obj:`numpy.ndarray`: A 2-dimensional Numpy array containing the integer + color values for each pixel in the surface. + + Raises: + RuntimeError: If Numpy could not be imported. + + """ + sf, sz, shape, dtype, strides = _ndarray_prep(source, "pixels2d", ndim=2) + if SDL_MUSTLOCK(sf): + SDL_LockSurface(sf) + + pxbuf = ctypes.cast(sf.pixels, ctypes.POINTER(ctypes.c_ubyte * sz)) + arr = SurfaceArray( + shape, dtype, pxbuf.contents, 0, strides, "C", source, sf + ) return arr.transpose() if transpose else arr -@experimental def pixels3d(source, transpose=True): - """Creates a 3D pixel array from the passed source. - """ - if not _HASNUMPY: - raise UnsupportedError(pixels3d, "numpy module could not be loaded") + """Creates a 3D Numpy array view for a given SDL surface. - psurface = _get_target_surface(source, argname="source") - bpp = psurface.format.contents.BytesPerPixel - if bpp < 1 or bpp > 4: - raise ValueError("unsupported bpp") - strides = (psurface.pitch, bpp, 1) - sz = psurface.h * psurface.pitch - shape = psurface.h, psurface.w, bpp + This function casts the surface pixels to a 3D Numpy array view, providing + read and write access to the underlying surface. If the source surface is + RLE-accelerated, it will be locked automatically when the view is created + and you will need to re-lock the surface using :func:`SDL_UnlockSurface` + once you are done with the array. + + By default, the array is returned in ``arr[x][y]`` format with the x-axis + as the first dimension, contrary to PIL and PyOpenGL convention. To obtain + an ``arr[y][x]`` array, set the ``transpose`` argument to ``False``. + + When creating a 3D array view, the order of the RGBA values for each pixel + may be reversed for some common surface pixel formats (e.g. 'BGRA' for a + ``SDL_PIXELFORMAT_ARGB8888`` surface). To correct this, you can call + ``numpy.flip(arr, axis=2)`` to return a view of the array with the expected + channel order. + + .. warning:: + The source surface should not be freed or deleted until the array is no + longer needed. Accessing the array for a freed surface will likely cause + Python to hard-crash. - if SDL_MUSTLOCK(psurface): - SDL_LockSurface(psurface) - pxbuf = ctypes.cast(psurface.pixels, ctypes.POINTER(ctypes.c_ubyte * sz)) - arr = SurfaceArray(shape, numpy.uint8, pxbuf.contents, 0, strides, "C", - source, psurface) + .. note:: + This function requires Numpy to be installed in the current Python + environment. + + Args: + source (:obj:`~sdl2.SDL_Surface`, :obj:`~sdl2.ext.SoftwareSprite`): The + SDL surface to cast to a numpy array. + transpose (bool, optional): Whether the output array should be + transposed to have ``arr[x][y]`` axes instead of ``arr[y][x]`` axes. + Defaults to ``True``. + + Returns: + :obj:`numpy.ndarray`: A 3-dimensional Numpy array containing the values + of each byte for each pixel in the surface. + + Raises: + RuntimeError: If Numpy could not be imported. + + """ + sf, sz, shape, dtype, strides = _ndarray_prep(source, "pixels3d", ndim=3) + if SDL_MUSTLOCK(sf): + SDL_LockSurface(sf) + + pxbuf = ctypes.cast(sf.pixels, ctypes.POINTER(ctypes.c_ubyte * sz)) + arr = SurfaceArray( + shape, dtype, pxbuf.contents, 0, strides, "C", source, sf + ) return arr.transpose(1, 0, 2) if transpose else arr + + +def surface_to_ndarray(source, ndim=3): + """Returns a copy of an SDL surface as a Numpy array. + + The main difference between this function and :func:`~sdl2.ext.pixels2d` or + :func:`~sdl2.ext.pixels3d` is that it returns a copy of the surface instead + of a view, meaning that modifying the returned array will not affect the + original surface (or vice-versa). This function is also slightly safer, + as it does not assume that the source surface has been kept in memory. + + When creating a 3D array copy, the order of the RGBA values for each pixel + may be reversed for some common surface pixel formats (e.g. 'BGRA' for a + ``SDL_PIXELFORMAT_ARGB8888`` surface). To correct this, you can call + ``numpy.flip(arr, axis=2)`` to return a view of the array with the expected + channel order. + + .. note:: + Unlike :func:`~sdl2.ext.pixels2d` or :func:`~sdl2.ext.pixels3d`, this + function always returns arrays with the y-axis as the first dimension + (e.g. ``arr[y][x]``). + + .. note:: + This function requires Numpy to be installed in the current Python + environment. + + Args: + source (:obj:`~sdl2.SDL_Surface`, :obj:`~sdl2.ext.SoftwareSprite`): The + SDL surface to convert to a numpy array. + ndim (int, optional): The number of dimensions for the returned array, + must be either 2 (for a 2D array) or 3 (for a 3D array). Defaults + to 3. + + Returns: + :obj:`numpy.ndarray`: A Numpy array containing a copy of the pixel data + for the given surface. + + Raises: + RuntimeError: If Numpy could not be imported. + + """ + if ndim not in [2, 3]: + err = "Can only convert surfaces to 2D or 3D arrays (got {0})." + raise ValueError(err.format(ndim)) + funcname = "surface_to_array" + sf, sz, shape, dtype, strides = _ndarray_prep(source, funcname, ndim) + was_unlocked = sf.locked == 0 + if SDL_MUSTLOCK(sf): + SDL_LockSurface(sf) + + pxbuf = ctypes.cast(sf.pixels, ctypes.POINTER(ctypes.c_ubyte * sz)) + tmp = numpy.ndarray(shape, dtype, pxbuf.contents, strides=strides) + if was_unlocked and SDL_MUSTLOCK(sf): + SDL_UnlockSurface(sf) + + return numpy.copy(tmp) + + +class SurfaceArray(numpy.ndarray if _HASNUMPY else object): + """A Numpy array that keeps a reference to its parent SDL surface. + + This class is used to keep track of the original source object for + :func:`~sdl2.ext.pixels2d` or :func:`~sdl2.ext.pixels3d` to prevent it from + being automatically freed during garbage collection. It should never be used + for any other purpose. + + """ + def __new__(cls, shape, dtype=float, buffer_=None, offset=0, + strides=None, order=None, source=None, surface=None): + if _HASNUMPY: + sfarray = numpy.ndarray.__new__( + cls, shape, dtype, buffer_, offset, strides, order + ) + sfarray._source = source + sfarray._surface = surface + return sfarray + else: + return None + + def __array_finalize__(self, sfarray): + if sfarray is None: + return + self._source = getattr(sfarray, '_source', None) + self._surface = getattr(sfarray, '_surface', None) diff --git a/sdl2/ext/renderer.py b/sdl2/ext/renderer.py new file mode 100644 index 00000000..fd67d10c --- /dev/null +++ b/sdl2/ext/renderer.py @@ -0,0 +1,778 @@ +from ctypes import byref, c_int, c_float + +from .. import blendmode, surface, rect, video, render, error, dll, hints +from ..stdinc import Uint8, Uint32 + +from .color import convert_to_color +from .common import raise_sdl_err +from .compat import deprecated, stringify, byteify, isiterable +from .sprite import SoftwareSprite, TextureSprite +from .surface import _get_target_surface +from .window import Window + +__all__ = ["set_texture_scale_quality", "Renderer", "Texture"] + + +def is_numeric(x): + try: + return float(x) == x + except TypeError: + return False + + +def _get_texture_size(texture): + flags = Uint32() + access, w, h = (c_int(), c_int(), c_int()) + ret = render.SDL_QueryTexture( + texture, byref(flags), byref(access), byref(w), byref(h) + ) + if ret < 0: + raise_sdl_err("getting texture attributes") + return (w.value, h.value) + + +def _sanitize_points(points): + # If first item is numeric, assume flat list of points + if isinstance(points, rect.SDL_Point) or isinstance(points, rect.SDL_FPoint): + points = [points] + elif is_numeric(points[0]): + if len(points) % 2 != 0: + raise ValueError("Flat x/y coordinate lists must have even length") + chunked = [] + for i in range(0, len(points), 2): + p = (points[i], points[i+1]) + chunked.append(p) + points = chunked + + type_err = ( + "Points must be specified as either (x, y) tuples or " + "SDL_Point objects (Got unsupported format '{0}')" + ) + pts = [] + if not isiterable(points): + points = [points] + for p in points: + if isinstance(p, rect.SDL_Point) or isinstance(p, rect.SDL_FPoint): + p = (p.x, p.y) + elif len(p) != 2: + raise ValueError(type_err.format(str(p))) + if dll.version < 2010 and any([int(v) != v for v in p]): + e = "Floating point rendering requires SDL 2.0.10 or newer" + raise RuntimeError(e + " (got '{0}')".format(str(p))) + pts.append((p[0], p[1])) + return pts + + +def _sanitize_rects(rects): + type_err = ( + "Rectangles must be specified as either (x, y, w, h) tuples or " + "SDL_Rect objects (Got unsupported format '{0}')" + ) + out = [] + if not isiterable(rects) or not isiterable(rects[0]): + rects = [rects] + for r in rects: + if isinstance(r, rect.SDL_Rect) or isinstance(r, rect.SDL_FRect): + r = (r.x, r.y, r.w, r.h) + elif len(r) != 4: + raise ValueError(type_err.format(str(r))) + if dll.version < 2010 and any([int(v) != v for v in r]): + e = "Floating point rendering requires SDL 2.0.10 or newer" + raise RuntimeError(e + " (got '{0}')".format(str(r))) + out.append((r[0], r[1], r[2], r[3])) + return out + + + +def set_texture_scale_quality(method): + """Sets the default scaling quailty for :obj:`~sdl2.ext.Texture` objects. + + By default, SDL2 uses low-quality nearest-neighbour scaling for all new + textures. This method lets you change the default scaling method to one of + the following options: + + ========= ============================================================= + Method Description + ========= ============================================================= + Nearest Nearest-neighbour pixel scaling (no filtering) + Linear Linear filtering + Best Anisotropic filtering (falls back to linear if not available) + ========= ============================================================= + + This function does not apply retroactively, and will only affect textures + created after it is called. + + Args: + method (str): The default scaling method to use for SDL textures. Must + be one of 'nearest', 'linear', or 'best'. + + """ + method = byteify(method.lower()) + if method not in [b"nearest", b"linear", b"best"]: + raise ValueError( + "Texture scaling method must be 'nearest', 'linear', or 'best'." + ) + hints.SDL_SetHint(hints.SDL_HINT_RENDER_SCALE_QUALITY, method) + + + +class Texture(object): + """A 2D texture to be used with a :obj:`~sdl2.ext.Renderer`. + + In SDL2, textures are 2D images that have been prepared for fast rendering + with a given renderer. For example, if an SDL surface is converted into a + texture with a :obj:`~sdl2.ext.Renderer` using the OpenGL backend, the pixel + data for the surface will internally be converted into an OpenGL texture. + + Once a surface has been converted into a :obj:`Texture`, the surface can be + safely deleted if no longer needed. + + Args: + renderer (:obj:`~sdl2.ext.Renderer`): The renderer associated with the + texture. + surface (:obj:`~sdl2.SDL_Surface`): An SDL surface from which the + texture will be created. + + """ + def __init__(self, renderer, surface): + # Validate and get reference to the parent renderer + self._renderer_ref = None + if isinstance(renderer, Renderer): + self._renderer_ref = renderer._renderer_ref + elif hasattr(renderer, "contents"): + if isinstance(renderer.contents, render.SDL_Renderer): + self._renderer_ref = [renderer] + if self._renderer_ref is None: + raise TypeError( + "'renderer' must be a valid Renderer object or a pointer to " + "an SDL_Renderer." + ) + # Convert the passed surface into a texture + surface = _get_target_surface(surface, "surface") + self._tx = render.SDL_CreateTextureFromSurface(self._renderer, surface) + if not self._tx: + raise_sdl_err("creating the texture") + + def __del__(self): + if hasattr(self, "_tx"): + self.destroy() + + @property + def _renderer(self): + # NOTE: This is sort of a hack to get around a ctypes problem. Basically, + # if the parent renderer of a texture is destroyed before the texture + # itself, destroying or doing anything with that texture will result in a + # segfault. By wrapping the renderer in a List, we can overwrite it with + # None when destroyed (within the Renderer class) and detect that in any + # child textures to avoid any memory-unsafe behaviour. + return self._renderer_ref[0] + + @property + def tx(self): + """:obj:`~sdl2.SDL_Texture`: The underlying base SDL texture object. + Can be used to perform operations with the texture using the base + PySDL2 bindings. + + """ + if self._tx is None: + raise RuntimeError( + "Cannot use a texture after it has been destroyed." + ) + elif self._renderer_ref[0] is None: + raise RuntimeError( + "Cannot use a texture after its parent renderer has been " + "destroyed." + ) + return self._tx + + @property + def size(self): + """tuple: The width and height (in pixels) of the texture.""" + return _get_texture_size(self.tx) + + def destroy(self): + """Deletes the texture and frees its associated memory. + + When a texture is no longer needed, it should be destroyed using this + method to free up memory for new textures. After being destroyed, a + texture can no longer be used. + + """ + if self._tx and self._renderer_ref[0]: + render.SDL_DestroyTexture(self._tx) + self._tx = None + + @property + def scale_mode(self): + if dll.version < 2012: + return "unknown" + modes = ["nearest", "linear", "best"] + mode_num = c_int() + ret = render.SDL_GetTextureScaleMode(self.tx, byref(mode_num)) + if ret < 0: + raise_sdl_err("retrieving the texture scaling mode") + try: + return modes[mode_num.value] + except IndexError: + return "unknown" + + def set_scale_mode(self, mode): + if dll.version < 2012: + return None + modes = { + "nearest": render.SDL_ScaleModeNearest, + "linear": render.SDL_ScaleModeLinear, + "best": render.SDL_ScaleModeBest, + } + mode = mode.lower() + if mode not in modes.keys(): + raise ValueError( + "Texture scaling mode must be either 'nearest', 'linear', " + "or 'best'." + ) + ret = render.SDL_SetTextureScaleMode(self.tx, modes[mode]) + if ret < 0: + raise_sdl_err("setting the texture scaling mode") + + + +class Renderer(object): + """A rendering context for SDL2 windows and sprites. + + A ``Renderer`` can be created from an SDL window, an SDL Surface, or a + :obj:`~sdl2.ext.SoftwareSprite`. Depending on the settings and operating + system, this rendering context can use either hardware or software + acceleration. + + A useful feature of SDL renderers is that that the logical size of the + rendering context can be different than the actual size (in pixels) of its + target window or surface. For example, the renderer for a 1024x768 window + can be set to have a logical size of 640x480, improving performance at the + cost of image quality. The rendered content will be automatically scaled to + fit the target, and will be centered with black bars on either side in the + case of an aspect ratio mismatch. + + If creating a rendering context from a window, you can customize the + renderer using flags to request different settings: + + =============================== =========================================== + Flag Description + =============================== =========================================== + ``SDL_RENDERER_SOFTWARE`` Requests a software-accelerated renderer. + ``SDL_RENDERER_ACCELERATED`` Requests a hardware-accelerated renderer. + ``SDL_RENDERER_PRESENTVSYNC`` Enables vsync support for :meth:`present`. + ``SDL_RENDERER_TARGETTEXTURE`` Requests support for rendering to texture. + =============================== =========================================== + + To combine multiple flags, you can use a bitwise OR to combine two or more + together before passing them to the `flags` argument:: + + render_flags = ( + sdl2.SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC + ) + sdl2.ext.Renderer(window, flags=render_flags) + + By default, SDL2 will choose the first renderer backend that supports all + the requested flags. However, you can also request a specific rendering + backend by name (e.g. 'opengl', 'opengles2', 'metal', 'direct3d', etc.), + giving you more control but likely making your code less cross-platform. + + Args: + target (:obj:`~sdl2.ext.Window`, :obj:`~sdl2.SDL_Surface`): The target + window or surface from which to create the rendering context. + backend (str or int, optional): The name of the specific backend to use + for the new rendering context (e.g. 'opengl'). Defaults to letting + SDL2 decide. If ``target`` is not an SDL window, this argument has + no effect. + logical_size (tuple, optional): The initial logical size (in pixels) of + the rendering context as a ``(w, h)`` tuple. Defaults to the size + of the target window or surface. + flags (int, optional): The requested features and settings for the + new rendering context. Defaults to requesting a hardware-accelerated + context. If ``target`` is not an SDL window, this argument has no + effect. + + Raises: + RuntimeError: If a requested rendering backend is not available. + + """ + + def __init__(self, target, backend=-1, logical_size=None, + flags=render.SDL_RENDERER_ACCELERATED): + self._renderer_ref = None + self.rendertarget = None + + available = self._get_render_drivers() + if isinstance(backend, str): + if backend.lower() in available: + index = available.index(backend.lower()) + else: + e1 = "'{0}' is not a supported renderer on this system. " + e2 = "The renderer backend must be one of the following: " + raise RuntimeError(e1.format(backend) + e2 + str(available)) + else: + index = backend + + if isinstance(target, Window): + _renderer = render.SDL_CreateRenderer(target.window, index, flags) + elif isinstance(target, video.SDL_Window): + _renderer = render.SDL_CreateRenderer(target, index, flags) + elif isinstance(target, SoftwareSprite): + _renderer = render.SDL_CreateSoftwareRenderer(target.surface) + elif isinstance(target, surface.SDL_Surface): + _renderer = render.SDL_CreateSoftwareRenderer(target) + elif "SDL_Surface" in str(type(target)): + _renderer = render.SDL_CreateSoftwareRenderer(target.contents) + else: + raise TypeError("unsupported target type") + if not _renderer: + raise_sdl_err("creating the SDL renderer") + self._renderer_ref = [_renderer] + + self.rendertarget = target + self.color = (0, 0, 0, 0) # Set black as the default draw color + self._original_logical_size = self.logical_size + if logical_size is not None: + self.logical_size = logical_size + + def __del__(self): + if self._renderer_ref is not None: + self.destroy() + + def _get_render_drivers(self): + renderers = [] + drivers = render.SDL_GetNumRenderDrivers() + for x in range(drivers): + info = render.SDL_RendererInfo() + ret = render.SDL_GetRenderDriverInfo(x, info) + if ret != 0: + renderers.append(stringify(info.name)) + error.SDL_ClearError() + return renderers + + @property + def sdlrenderer(self): + """:obj:`~sdl2.SDL_Renderer`: The underlying base SDL renderer object. + Can be used to perform operations with the renderer using the base + PySDL2 bindings. + + """ + if self._renderer_ref[0] is None: + raise RuntimeError( + "Cannot use a renderer after it has been destroyed." + ) + return self._renderer_ref[0] + + @property + @deprecated + def renderer(self): + return self.sdlrenderer + + @property + def logical_size(self): + """tuple: The logical size of the rendering context (in pixels), as a + ``(width, height)`` tuple. + + """ + w, h = c_int(), c_int() + render.SDL_RenderGetLogicalSize(self.sdlrenderer, byref(w), byref(h)) + return w.value, h.value + + @logical_size.setter + def logical_size(self, size): + width, height = size + ret = render.SDL_RenderSetLogicalSize(self.sdlrenderer, width, height) + if ret != 0: + raise_sdl_err("setting the logical size of the renderer") + + @property + def color(self): + """:obj:`~sdl2.ext.Color`: The current drawing color of the renderer.""" + r, g, b, a = Uint8(), Uint8(), Uint8(), Uint8() + ret = render.SDL_GetRenderDrawColor(self.sdlrenderer, byref(r), byref(g), + byref(b), byref(a)) + if ret < 0: + raise_sdl_err("retrieving the drawing color of the renderer") + return convert_to_color((r.value, g.value, b.value, a.value)) + + @color.setter + def color(self, value): + c = convert_to_color(value) + ret = render.SDL_SetRenderDrawColor(self.sdlrenderer, c.r, c.g, c.b, c.a) + if ret < 0: + raise_sdl_err("setting the drawing color of the renderer") + + @property + def blendmode(self): + """int: The blend mode used for :meth:`fill` and :meth:`line` drawing + operations. This value can be any of the following constants: + + ========================= ==================================== + Flag Description + ========================= ==================================== + ``SDL_BLENDMODE_NONE`` No blending + ``SDL_BLENDMODE_BLEND`` Alpha channel blending + ``SDL_BLENDMODE_ADD`` Additive blending + ``SDL_BLENDMODE_MOD`` Color modulation + ``SDL_BLENDMODE_MUL`` Color multiplication (SDL >= 2.0.12) + ========================= ==================================== + + """ + mode = blendmode.SDL_BlendMode() + ret = render.SDL_GetRenderDrawBlendMode(self.sdlrenderer, byref(mode)) + if ret < 0: + raise_sdl_err("retrieving the blend mode for the renderer") + return mode + + @blendmode.setter + def blendmode(self, value): + ret = render.SDL_SetRenderDrawBlendMode(self.sdlrenderer, value) + if ret < 0: + raise_sdl_err("setting the blend mode for the renderer") + + @property + def scale(self): + """tuple: The x/y scaling factors applied to all drawing coordinates + before rendering, in the format ``(scale_x, scale_y)``. These can be + used to facilitate resolution-independent drawing. + + """ + sx = c_float(0.0) + sy = c_float(0.0) + render.SDL_RenderGetScale(self.sdlrenderer, byref(sx), byref(sy)) + return sx.value, sy.value + + @scale.setter + def scale(self, value): + if any([s <= 0 for s in value]): + raise ValueError("Scaling factors must be greater than zero.") + sx, sy = value + ret = render.SDL_RenderSetScale(self.sdlrenderer, sx, sy) + if ret != 0: + raise_sdl_err("setting the scaling factors for the renderer") + + def destroy(self): + """Destroys the renderer and any associated textures. + + When a renderer is no longer needed, it should be destroyed using this + method to free up its associated memory. After being destroyed, a + renderer can no longer be used. + + """ + if self._renderer_ref[0]: + render.SDL_DestroyRenderer(self._renderer_ref[0]) + self._renderer_ref[0] = None + self.rendertarget = None + + def reset_logical_size(self): + """Resets the logical size of the renderer to its original value.""" + self.logical_size = self._original_logical_size + + def clear(self, color=None): + """Clears the rendering surface with a given color. + + Args: + color (:obj:`~sdl2.ext.Color`, optional): The color with which to + clear the entire rendering context. If not specified, the + renderer's current :attr:`color` will be used. + + """ + if color is None: + ret = render.SDL_RenderClear(self.sdlrenderer) + else: + tmp = self.color + self.color = color + ret = render.SDL_RenderClear(self.sdlrenderer) + self.color = tmp + if ret < 0: + raise_sdl_err("clearing the rendering context") + + def copy(self, src, srcrect=None, dstrect=None, angle=0, center=None, + flip=render.SDL_FLIP_NONE): + """Copies (blits) a texture to the rendering context. + + If the source texture is an :obj:`~sdl2.SDL_Surface`, you will need + to convert it into a :obj:`~sdl2.ext.Texture` first before it can be + copied to the rendering surface. + + The source texture can be flipped horizontally or vertically when + being copied to the rendering context using one of the following + flags: + + ========================= =================================== + Flag Description + ========================= =================================== + ``SDL_FLIP_NONE`` Does not flip the source (default) + ``SDL_FLIP_HORIZONTAL`` Flips the source horizontally + ``SDL_FLIP_VERTICAL`` Flips the source vertically + ========================= =================================== + + .. note:: + Subpixel rendering (i.e. using floats as pixel coordinates) requires + SDL 2.0.10 or newer. + + Args: + src (:obj:`~sdl2.ext.Texture`, :obj:`~sdl2.SDL_Texture`): The source + texture to copy to the rendering surface. + srcrect (tuple, optional): An ``(x, y, w, h)`` rectangle defining + the subset of the source texture to copy to the rendering + surface. Defaults to copying the entire source texture. + dstrect (tuple, optional): An ``(x, y, w, h)`` rectangle + defining the region of the rendering surface to which the + source texture will be copied. Alternatively, if only + ``(x, y)`` coordinates are provided, the width and height of + the source rectangle will be used. Defaults to stretching + the source across the entire rendering context. + angle (float, optional): The clockwise rotation (in degrees) to + apply to the destination rectangle. Defaults to no rotation. + center (tuple, optional): The point around with the destination + rectangle will be rotated. Defaults to the center of the + destination rectangle. + flip (int, optional): A flag indicating whether the source should + be flipped (horizontally or vertically) when rendering to the + render context. Defaults to no flipping. + + """ + if dll.version < 2010: + render_copy = render.SDL_RenderCopyEx + Point = rect.SDL_Point + Rect = rect.SDL_Rect + else: + render_copy = render.SDL_RenderCopyExF + Point = rect.SDL_FPoint + Rect = rect.SDL_FRect + + if isinstance(src, TextureSprite): + texture = src.texture + angle = angle if angle != 0 else src.angle + center = center if center else src.center + flip = flip if flip != 0 else src.flip + elif isinstance(src, Texture): + texture = src.tx + elif isinstance(src, render.SDL_Texture): + texture = src + else: + raise TypeError("src must be a Texture object or an SDL_Texture") + + if srcrect: + x, y, w, h = _sanitize_rects([srcrect])[0] + srcrect = rect.SDL_Rect(int(x), int(y), int(w), int(h)) + + if dstrect: + if len(dstrect) == 2: + x, y = _sanitize_points([dstrect])[0] + if srcrect: + w, h = (srcrect.w, srcrect.h) + else: + w, h = _get_texture_size(texture) + elif len(dstrect) == 4: + x, y, w, h = _sanitize_rects([dstrect])[0] + dstrect = Rect(x, y, w, h) + + if center: + x, y = _sanitize_points(center)[0] + center = Point(x, y) + + ret = render_copy( + self.sdlrenderer, texture, srcrect, dstrect, angle, center, flip + ) + if ret < 0: + raise_sdl_err("copying the texture to the rendering context") + + def blit(self, src, srcrect=None, dstrect=None, angle=0, center=None, + flip=render.SDL_FLIP_NONE): + """Copies a texture to the rendering context. + + An alias for the :meth:`copy` method. + + """ + self.copy(src, srcrect, dstrect, angle, center, flip) + + def present(self): + """Presents the current rendering surface to the screen. + + Because SDL renderers use batch rendering (i.e. they have a separate + backbuffer that is drawn to and buffer shown on screen which are + switched when this function is called), any drawing operations + performed with the renderer will not take effect until this method + is called. + + It is recommended that you clear and redraw the contents of the + rendering context every time before this method is called, as the + contents of the buffers are not guaranteed to remain the same between + repeat presentations. + + """ + render.SDL_RenderPresent(self.sdlrenderer) + + def draw_line(self, points, color=None): + """Draws one or more connected lines on the rendering context. + + .. note:: + Subpixel rendering (i.e. using floats as pixel coordinates) requires + SDL 2.0.10 or newer. + + Args: + points (list): A list of 2 or more ``(x, y)`` coordinates or + :obj:`~sdl2.SDL_Point` objects defining the set of connected + lines to draw. + color (:obj:`~sdl2.ext.Color`, optional): The color with which to + draw the lines. If not specified, the renderer's current + :attr:`color` will be used. + + """ + if dll.version < 2010: + draw_lines = render.SDL_RenderDrawLines + Point = rect.SDL_Point + else: + draw_lines = render.SDL_RenderDrawLinesF + Point = rect.SDL_FPoint + + points = _sanitize_points(points) + if len(points) < 2: + raise ValueError("At least two (x, y) points are required.") + sdlpts = [] + for p in points: + x, y = p + sdlpts.append(Point(x, y)) + points_ptr = (Point * len(points))(*sdlpts) + + if color is None: + ret = draw_lines(self.sdlrenderer, points_ptr, len(points)) + else: + tmp = self.color + self.color = color + ret = draw_lines(self.sdlrenderer, points_ptr, len(points)) + self.color = tmp + + if ret < 0: + raise_sdl_err("drawing lines to the renderer") + + def draw_point(self, points, color=None): + """Draws one or more points on the rendering context. + + .. note:: + Subpixel rendering (i.e. using floats as pixel coordinates) requires + SDL 2.0.10 or newer. + + Args: + points (list): A list of ``(x, y)`` coordinates or + :obj:`~sdl2.SDL_Point` objects defining the set of points to + draw. + color (:obj:`~sdl2.ext.Color`, optional): The color with which to + draw the points. If not specified, the renderer's current + :attr:`color` will be used. + + """ + if dll.version < 2010: + draw_points = render.SDL_RenderDrawPoints + Point = rect.SDL_Point + else: + draw_points = render.SDL_RenderDrawPointsF + Point = rect.SDL_FPoint + + points = _sanitize_points(points) + sdlpts = [] + for p in points: + x, y = p + sdlpts.append(Point(x, y)) + points_ptr = (Point * len(points))(*sdlpts) + + if color is None: + ret = draw_points(self.sdlrenderer, points_ptr, len(points)) + else: + tmp = self.color + self.color = color + ret = draw_points(self.sdlrenderer, points_ptr, len(points)) + self.color = tmp + + if ret < 0: + raise_sdl_err("drawing points to the renderer") + + def draw_rect(self, rects, color=None): + """Draws one or more rectangles on the rendering context. + + Rectangles can be specified as 4-item ``(x, y, w, h)`` tuples, + :obj:`~sdl2.rect.SDL_Rect` objects, or a list containing multiple + rectangles in either format. + + .. note:: + Subpixel rendering (i.e. using floats as pixel coordinates) requires + SDL 2.0.10 or newer. + + Args: + rects (tuple, :obj:`~sdl2.SDL_Rect`, list): The rectangle(s) to draw + to the rendering context. + color (:obj:`~sdl2.ext.Color`, optional): The color with which to + draw the rectangle(s). If not specified, the renderer's current + :attr:`color` will be used. + + """ + if dll.version < 2010: + draw_rects = render.SDL_RenderDrawRects + Rect = rect.SDL_Rect + else: + draw_rects = render.SDL_RenderDrawRectsF + Rect = rect.SDL_FRect + + rects = _sanitize_rects(rects) + sdlrects = [] + for r in rects: + x, y, w, h = r + sdlrects.append(Rect(x, y, w, h)) + rects_ptr = (Rect * len(rects))(*sdlrects) + + if color is None: + ret = draw_rects(self.sdlrenderer, rects_ptr, len(rects)) + else: + tmp = self.color + self.color = color + ret = draw_rects(self.sdlrenderer, rects_ptr, len(rects)) + self.color = tmp + + if ret < 0: + raise_sdl_err("drawing rectangles to the renderer") + + def fill(self, rects, color=None): + """Fills one or more rectangular regions the rendering context. + + Fill regions can be specified as 4-item ``(x, y, w, h)`` tuples, + :obj:`~sdl2.rect.SDL_Rect` objects, or a list containing multiple + rectangles in either format. + + .. note:: + Subpixel rendering (i.e. using floats as pixel coordinates) requires + SDL 2.0.10 or newer. + + Args: + rects (tuple, :obj:`~sdl2.SDL_Rect`, list): The rectangle(s) to fill + within the rendering context. + color (:obj:`~sdl2.ext.Color`, optional): The color with which to + fill the rectangle(s). If not specified, the renderer's current + :attr:`color` will be used. + + """ + if dll.version < 2010: + fill_rects = render.SDL_RenderFillRects + Rect = rect.SDL_Rect + else: + fill_rects = render.SDL_RenderFillRectsF + Rect = rect.SDL_FRect + + rects = _sanitize_rects(rects) + sdlrects = [] + for r in rects: + x, y, w, h = r + sdlrects.append(Rect(x, y, w, h)) + rects_ptr = (Rect * len(rects))(*sdlrects) + + if color is None: + ret = fill_rects(self.sdlrenderer, rects_ptr, len(rects)) + else: + tmp = self.color + self.color = color + ret = fill_rects(self.sdlrenderer, rects_ptr, len(rects)) + self.color = tmp + + if ret < 0: + raise_sdl_err("filling rectangles in the renderer") diff --git a/sdl2/ext/resources.py b/sdl2/ext/resources.py index 21fba301..bb7ff918 100644 --- a/sdl2/ext/resources.py +++ b/sdl2/ext/resources.py @@ -19,17 +19,40 @@ import urllib2 +def _validate_path(path, what, write=False): + fullpath = os.path.abspath(path) + fname = os.path.basename(path) + if write: + parent = os.path.abspath(os.path.join(fullpath, os.pardir)) + if not os.path.isdir(parent): + e = "The given parent directory '{0}' does not exist" + raise IOError(e.format(parent)) + else: + if not os.path.exists(fullpath): + e = "Could not find {0} at the given path: {1}" + raise IOError(e.format(what, fullpath)) + return (fullpath, fname) + + def open_zipfile(archive, filename, directory=None): - """Opens and reads a certain file from a ZIP archive. + """Retrieves a given file from a ZIP archive. + + Args: + archive (:obj:`~zipfile.ZipFile`, str): The ZipFile object or path to + the ZIP archive containing the desired file. + filename (str): The name of the file to retrieve from the archive. + directory (str, optional): The path to the directory within the archive + containing the file to retrieve. Defaults to the root level of the + archive. - Opens and reads a certain file from a ZIP archive. The result is - returned as StringIO stream. filename can be a relative or absolute - path within the ZIP archive. The optional directory argument can be - used to supply a relative directory path, under which filename will - be searched. + Returns: + :obj:`~io.BytesIO`: A Python bytestream object containing the requested + file. + + Raises: + KeyError: If the given file could not be found within the archive. + TypeError: If the archive is not a valid ZIP archive. - If the filename could not be found, a KeyError will be raised. - Raises a TypeError, if archive is not a valid ZIP archive. """ data = None opened = False @@ -55,42 +78,46 @@ def open_zipfile(archive, filename, directory=None): def open_tarfile(archive, filename, directory=None, ftype=None): - """Opens and reads a certain file from a TAR archive. - - Opens and reads a certain file from a TAR archive. The result is - returned as StringIO stream. filename can be a relative or absolute - path within the TAR archive. The optional directory argument can be - used to supply a relative directory path, under which filename will - be searched. + """Retrieves a given file from a TAR archive. + + If the TAR archive uses ``.tar.gz`` or ``.tar.bz2`` compression and the + file name does not contain either of these identifiers, the compression + type must be manually specified. + + Args: + archive (:obj:`~tarfile.TarFile`, str): The TarFile object or path to + the TAR archive containing the desired file. + filename (str): The name of the file to retrieve from the archive. + directory (str, optional): The path to the directory within the archive + containing the file to retrieve. Defaults to the root level of the + archive. + ftype (str, optional): The compression type (if any) used for the TAR + file, can be either 'gz', 'bz2', or None (no compression). If not + specified, will default to assuming no compression. + + Returns: + :obj:`~io.BytesIO`: A Python bytestream object containing the requested + file. + + Raises: + KeyError: If the given file could not be found within the archive. + TypeError: If the archive is not a supported TAR archive. - ftype is used to supply additional compression information, in case - the system cannot determine the compression type itself, and can be - either 'gz' for gzip compression or 'bz2' for bzip2 compression. - - Note: - - If ftype is supplied, the compression mode will be enforced for - opening and reading. - - If the filename could not be found or an error occured on reading it, - None will be returned. - - Raises a TypeError, if archive is not a valid TAR archive or if type - is not a valid value of ('gz', 'bz2'). """ data = None opened = False - mode = 'r' - if ftype: - if ftype not in ('gz', 'bz2'): - raise TypeError("invalid TAR compression type") - mode = "r:%s" % ftype - if not isinstance(archive, tarfile.TarFile): if not tarfile.is_tarfile(archive): raise TypeError("passed file does not seem to be a TAR archive") else: + file_ext = archive.split('.')[-1] + if not ftype and file_ext in ('gz', 'bz2'): + ftype = file_ext + if ftype and ftype not in ('gz', 'bz2'): + e = "invalid TAR compression type '{0}' (must be 'gz' or 'bz2')" + raise TypeError(e.format(ftype)) + mode = 'r:{0}'.format(ftype) if ftype else 'r' archive = tarfile.open(archive, mode) opened = True @@ -108,14 +135,9 @@ def open_tarfile(archive, filename, directory=None, ftype=None): def open_url(filename, basepath=None): - """Opens and reads a certain file from a web or remote location. - - Opens and reads a certain file from a web or remote location. This - function utilizes the urllib2 module, which means that it is - restricted to the types of remote locations supported by urllib2. - - basepath can be used to supply an additional location prefix. - """ + # Opens and reads a certain file from a web or remote location. + # Deprecated because its argument names are confusing and because + # users are probably better off using urllib directly. url = filename if basepath: url = urlparse.urljoin(basepath, filename) @@ -123,17 +145,21 @@ def open_url(filename, basepath=None): class Resources(object): - """The Resources class manages a set of file resources and eases - accessing them by using relative paths, scanning archives - automatically and so on. + """A container class for managing application resource files. + + This class eases access to resources by allowing access using relative + paths, scanning archives to locate files, and more. + + Args: + path (str, optional): The path of a resource directory with which to + initialze the container. Defaults to ``None``. + subdir (str, optional): Deprecated, do not use. + excludepattern (str, optional): A regular expression indicating + which directories (if any) to ignore if initializing the + container with a resource path. Defaults to ``None``. + """ def __init__(self, path=None, subdir=None, excludepattern=None): - """Creates a new resource container instance. - - If path is provided, the resource container will scan the path - and add all found files to itself by invoking - scan(path, subdir, excludepattern). - """ self.files = {} if path: self.scan(path, subdir, excludepattern) @@ -158,11 +184,13 @@ def _scantar(self, filename, ftype=None): """ if not tarfile.is_tarfile(filename): raise TypeError("file '%s' is not a valid TAR archive" % filename) - mode = 'r' - if ftype: - if ftype not in ('gz', 'bz2'): - raise TypeError("invalid TAR compression type") - mode = "r:%s" % ftype + file_ext = filename.split('.')[-1] + if not ftype and file_ext in ('gz', 'bz2'): + ftype = file_ext + if ftype and ftype not in ('gz', 'bz2'): + e = "invalid TAR compression type '{0}' (must be 'gz' or 'bz2')" + raise TypeError(e.format(ftype)) + mode = 'r:{0}'.format(ftype) if ftype else 'r' archname = os.path.abspath(filename) archtype = 'tar' if ftype: @@ -176,9 +204,16 @@ def _scantar(self, filename, ftype=None): def add(self, filename): """Adds a file to the Resources container. - Depending on the file type (determined by the file suffix or name), - the file will be automatically scanned (if it is an archive) or - checked for availability (if it is a stream/network resource). + If the given file is a supported archive, its contents will be scanned + and added to the container. + + Args: + filename (str): The filepath of the resource to add to the + container. + + Raises: + ValueError: If the file does not exist at the provided path. + """ if not os.path.exists(filename): raise ValueError("invalid file path") @@ -190,10 +225,18 @@ def add(self, filename): self.add_file(filename) def add_file(self, filename): - """Adds a file to the Resources container. + """Adds a file without scanning to the Resources container. + + Unlike :meth:`add`, this method will not attempt to add the contents + of any provided archives to the container. + + Args: + filename (str): The filepath of the resource to add to the + container. + + Raises: + ValueError: If the file does not exist at the provided path. - This will only add the passed file and do not scan an archive or - check a stream for availability. """ if not os.path.exists(filename): raise ValueError("invalid file path") @@ -204,28 +247,57 @@ def add_file(self, filename): self.files[fname] = (None, None, abspath) def add_archive(self, filename, typehint='zip'): - """Adds an archive file to the Resources container. + """Adds a ``.zip`` or ``.tar`` archive to the container. This will scan the passed archive and add its contents to the - list of available resources. + list of available resources. Currently ``.zip``, ``.tar``, + ``.tar.bz2``, and ``.tar.gz`` formats are supported. + + Args: + filename (str): The filepath of the archive to scan and add to the + container. + typehint (str, optional): The format of the archive to add to the + container, required if using a custom file extension. Must be + one of ``zip``, ``tar``, ``tarbz2``, or ``targz``. Defaults to + ``zip`` if not specified. + + Raises: + ValueError: If the file does not exist at the provided path, or if + the file is not a supported archive type. + """ if not os.path.exists(filename): raise ValueError("invalid file path") - if typehint == 'zip': + fname = os.path.basename(filename) + if 'zip' in fname.split('.'): self._scanzip(filename) - elif typehint == 'tar': + elif 'tar' in fname.split('.'): self._scantar(filename) - elif typehint == 'tarbz2': - self._scantar(filename, 'bz2') - elif typehint == 'targz': - self._scantar(filename, 'gz') else: - raise ValueError("unsupported archive type") + if typehint == 'zip': + self._scanzip(filename) + elif typehint == 'tar': + self._scantar(filename) + elif typehint == 'tarbz2': + self._scantar(filename, 'bz2') + elif typehint == 'targz': + self._scantar(filename, 'gz') + else: + raise ValueError("unsupported archive type") def get(self, filename): - """Gets a specific file from the Resources. + """Retrieves a resource file by name from the container. + + Args: + filename (str): The file name of the resource to retrieve. + + Returns: + :obj:`~io.BytesIO`: A Python bytestream object containing the + retrieved resource file. + + Raises: + KeyError: If the given file could not be found. - Raises a KeyError, if filename could not be found. """ archive, ftype, pathname = self.files[filename] if archive: @@ -245,14 +317,7 @@ def get(self, filename): return data def get_filelike(self, filename): - """Like get(), but tries to return the original file handle, if - possible. - - If the passed filename is only available within an archive, a - StringIO instance will be returned. - - Raises a KeyError, if filename could not be found. - """ + # Deprecated, doesn't make much difference in Python 3 archive, ftype, pathname = self.files[filename] if archive: if ftype == 'zip': @@ -268,12 +333,21 @@ def get_filelike(self, filename): return open(pathname, 'rb') def get_path(self, filename): - """Gets the path of the passed filename. + """Gets the path of a given resource file. - If filename is only available within an archive, a string in - the form 'filename@archivename' will be returned. + If the file is only available within an archive, a string in the form + ``filename@archivename`` will be returned. + + Args: + filename (str): The file name of the resource to locate. + + Returns: + str: The absolute path of the resource file, or the archive + identifier string if the resource is inside an archive. + + Raises: + KeyError: If the given file could not be found. - Raises a KeyError, if filename could not be found. """ archive, ftype, pathname = self.files[filename] if archive: @@ -281,21 +355,21 @@ def get_path(self, filename): return pathname def scan(self, path, subdir=None, excludepattern=None): - """Scans a path and adds all found files to the Resources - container. + """Scans a path, adding all matching files to the container. + + If a located file is a ``.zip`` or ``.tar`` archive, its + contents will be indexed and added to the container automatically. - Scans a path and adds all found files to the Resources - container. If a file is a supported (ZIP or TAR) archive, its - contents will be indexed and added automatically. + Args: + path (str): The path of the directory to scan. + subdir (str, optional): Deprecated, do not use. + excludepattern (str, optional): A regular expression indicating + which directories (if any) within the file structure of the + given path to exclude from indexing. Defaults to ``None``. - The method will consider the directory part (os.path.dirname) of - the provided path as path to scan, if the path is not a - directory. If subdir is provided, it will be appended to the - path and used as starting point for adding files to the - Resources container. + Raises: + ValueError: If the specified path does not exist. - excludepattern can be a regular expression to skip directories, which - match the pattern. """ match = None if excludepattern: diff --git a/sdl2/ext/sprite.py b/sdl2/ext/sprite.py index 1aaa5f41..ba5823ce 100644 --- a/sdl2/ext/sprite.py +++ b/sdl2/ext/sprite.py @@ -1,303 +1,13 @@ -"""Sprite, texture and pixel surface routines.""" import abc -from ctypes import byref, cast, POINTER, c_int, c_float -from .common import SDLError -from .compat import * -from .color import convert_to_color -from .ebs import System -from .surface import subsurface -from .window import Window -from .image import load_image -from .. import blendmode, surface, rect, video, pixels, render, rwops -from ..stdinc import Uint8, Uint32 - -__all__ = ["Sprite", "SoftwareSprite", "TextureSprite", "SpriteFactory", - "SoftwareSpriteRenderSystem", "SpriteRenderSystem", - "TextureSpriteRenderSystem", "Renderer", "TEXTURE", "SOFTWARE" - ] - -TEXTURE = 0 -SOFTWARE = 1 - - -class Renderer(object): - """SDL2-based renderer for windows and sprites.""" - def __init__(self, target, index=-1, logical_size=None, - flags=render.SDL_RENDERER_ACCELERATED): - """Creates a new Renderer for the given target. - - If target is a Window or SDL_Window, index and flags are passed - to the relevant sdl.render.create_renderer() call. If target is - a SoftwareSprite or SDL_Surface, the index and flags arguments are - ignored. - """ - self.sdlrenderer = None - self.rendertarget = None - if isinstance(target, Window): - self.sdlrenderer = render.SDL_CreateRenderer(target.window, index, - flags) - elif isinstance(target, video.SDL_Window): - self.sdlrenderer = render.SDL_CreateRenderer(target, index, flags) - elif isinstance(target, SoftwareSprite): - self.sdlrenderer = render.SDL_CreateSoftwareRenderer(target.surface) - elif isinstance(target, surface.SDL_Surface): - self.sdlrenderer = render.SDL_CreateSoftwareRenderer(target) - elif "SDL_Surface" in str(type(target)): - self.sdlrenderer = render.SDL_CreateSoftwareRenderer(target.contents) - else: - raise TypeError("unsupported target type") - - self.rendertarget = target - if logical_size is not None: - self.logical_size = logical_size - - def __del__(self): - if self.sdlrenderer: - render.SDL_DestroyRenderer(self.sdlrenderer) - self.sdlrenderer = None - self.rendertarget = None - - @property - @deprecated - def renderer(self): - return self.sdlrenderer - - @property - def logical_size(self): - """The logical pixel size of the Renderer""" - w, h = c_int(), c_int() - render.SDL_RenderGetLogicalSize(self.sdlrenderer, byref(w), byref(h)) - return w.value, h.value - - @logical_size.setter - def logical_size(self, size): - """The logical pixel size of the Renderer""" - width, height = size - ret = render.SDL_RenderSetLogicalSize(self.sdlrenderer, width, height) - if ret != 0: - raise SDLError() - - @property - def color(self): - """The drawing color of the Renderer.""" - r, g, b, a = Uint8(), Uint8(), Uint8(), Uint8() - ret = render.SDL_GetRenderDrawColor(self.sdlrenderer, byref(r), byref(g), - byref(b), byref(a)) - if ret == -1: - raise SDLError() - return convert_to_color((r.value, g.value, b.value, a.value)) - - @color.setter - def color(self, value): - """The drawing color of the Renderer.""" - c = convert_to_color(value) - ret = render.SDL_SetRenderDrawColor(self.sdlrenderer, c.r, c.g, c.b, c.a) - if ret == -1: - raise SDLError() - - @property - def blendmode(self): - """The blend mode used for drawing operations (fill and line).""" - mode = blendmode.SDL_BlendMode() - ret = render.SDL_GetRenderDrawBlendMode(self.sdlrenderer, byref(mode)) - if ret == -1: - raise SDLError() - return mode - - @blendmode.setter - def blendmode(self, value): - """The blend mode used for drawing operations (fill and line).""" - ret = render.SDL_SetRenderDrawBlendMode(self.sdlrenderer, value) - if ret == -1: - raise SDLError() +from ctypes import byref, c_int - @property - def scale(self): - """The horizontal and vertical drawing scale.""" - sx = c_float(0.0) - sy = c_float(0.0) - render.SDL_RenderGetScale(self.sdlrenderer, byref(sx), byref(sy)) - return sx.value, sy.value - - @scale.setter - def scale(self, value): - """The horizontal and vertical drawing scale.""" - ret = render.SDL_RenderSetScale(self.sdlrenderer, value[0], value[1]) - if ret != 0: - raise SDLError() - - def clear(self, color=None): - """Clears the renderer with the currently set or passed color.""" - if color is not None: - tmp = self.color - self.color = color - ret = render.SDL_RenderClear(self.sdlrenderer) - if color is not None: - self.color = tmp - if ret == -1: - raise SDLError() - - def copy(self, src, srcrect=None, dstrect=None, angle=0, center=None, - flip=render.SDL_FLIP_NONE): - """Copies (blits) the passed source to the target of the Renderer.""" - if isinstance(src, TextureSprite): - texture = src.texture - angle = angle or src.angle - center = center or src.center - flip = flip or src.flip - elif isinstance(src, render.SDL_Texture): - texture = src - else: - raise TypeError("src must be a TextureSprite or SDL_Texture") - if srcrect is not None: - x, y, w, h = srcrect - srcrect = rect.SDL_Rect(x, y, w, h) - if dstrect is not None: - x, y, w, h = dstrect - dstrect = rect.SDL_Rect(x, y, w, h) - ret = render.SDL_RenderCopyEx(self.sdlrenderer, texture, srcrect, - dstrect, angle, center, flip) - if ret == -1: - raise SDLError() - - def present(self): - """Refreshes the target of the Renderer.""" - render.SDL_RenderPresent(self.sdlrenderer) - - def draw_line(self, points, color=None): - """Draws one or multiple connected lines on the renderer.""" - # (x1, y1, x2, y2, ...) - pcount = len(points) - if (pcount % 2) != 0: - raise ValueError("points does not contain a valid set of points") - if pcount < 4: - raise ValueError("points must contain more that one point") - if pcount == 4: - if color is not None: - tmp = self.color - self.color = color - x1, y1, x2, y2 = points - ret = render.SDL_RenderDrawLine(self.sdlrenderer, x1, y1, x2, y2) - if color is not None: - self.color = tmp - if ret == -1: - raise SDLError() - else: - x = 0 - off = 0 - count = pcount // 2 - SDL_Point = rect.SDL_Point - ptlist = (SDL_Point * count)() - while x < pcount: - ptlist[off] = SDL_Point(points[x], points[x + 1]) - x += 2 - off += 1 - if color is not None: - tmp = self.color - self.color = color - ptr = cast(ptlist, POINTER(SDL_Point)) - ret = render.SDL_RenderDrawLines(self.sdlrenderer, ptr, count) - if color is not None: - self.color = tmp - if ret == -1: - raise SDLError() - - def draw_point(self, points, color=None): - """Draws one or multiple points on the renderer.""" - # (x1, y1, x2, y2, ...) - pcount = len(points) - if (pcount % 2) != 0: - raise ValueError("points does not contain a valid set of points") - if pcount == 2: - if color is not None: - tmp = self.color - self.color = color - ret = render.SDL_RenderDrawPoint(self.sdlrenderer, points[0], - points[1]) - if color is not None: - self.color = tmp - if ret == -1: - raise SDLError() - else: - x = 0 - off = 0 - count = pcount // 2 - SDL_Point = rect.SDL_Point - ptlist = (SDL_Point * count)() - while x < pcount: - ptlist[off] = SDL_Point(points[x], points[x + 1]) - x += 2 - off += 1 - if color is not None: - tmp = self.color - self.color = color - ptr = cast(ptlist, POINTER(SDL_Point)) - ret = render.SDL_RenderDrawPoints(self.sdlrenderer, ptr, count) - if color is not None: - self.color = tmp - if ret == -1: - raise SDLError() - - def draw_rect(self, rects, color=None): - """Draws one or multiple rectangles on the renderer.""" - SDL_Rect = rect.SDL_Rect - # ((x, y, w, h), ...) - if type(rects[0]) == int: - # single rect - if color is not None: - tmp = self.color - self.color = color - x, y, w, h = rects - ret = render.SDL_RenderDrawRect(self.sdlrenderer, SDL_Rect(x, y, w, h)) - if color is not None: - self.color = tmp - if ret == -1: - raise SDLError() - else: - x = 0 - rlist = (SDL_Rect * len(rects))() - for idx, r in enumerate(rects): - rlist[idx] = SDL_Rect(r[0], r[1], r[2], r[3]) - if color is not None: - tmp = self.color - self.color = color - ptr = cast(rlist, POINTER(SDL_Rect)) - ret = render.SDL_RenderDrawRects(self.sdlrenderer, ptr, len(rects)) - if color is not None: - self.color = tmp - if ret == -1: - raise SDLError() +from .. import surface, rect, render +from ..stdinc import Uint32 - def fill(self, rects, color=None): - """Fills one or multiple rectangular areas on the renderer.""" - SDL_Rect = rect.SDL_Rect - # ((x, y, w, h), ...) - if type(rects[0]) == int: - # single rect - if color is not None: - tmp = self.color - self.color = color - x, y, w, h = rects - ret = render.SDL_RenderFillRect(self.sdlrenderer, SDL_Rect(x, y, w, h)) - if color is not None: - self.color = tmp - if ret == -1: - raise SDLError() - else: - x = 0 - rlist = (SDL_Rect * len(rects))() - for idx, r in enumerate(rects): - rlist[idx] = SDL_Rect(r[0], r[1], r[2], r[3]) - if color is not None: - tmp = self.color - self.color = color - ptr = cast(rlist, POINTER(SDL_Rect)) - ret = render.SDL_RenderFillRects(self.sdlrenderer, ptr, len(rects)) - if color is not None: - self.color = tmp - if ret == -1: - raise SDLError() +from .common import SDLError +from .surface import subsurface +__all__ = ["Sprite", "SoftwareSprite", "TextureSprite"] class Sprite(object): @@ -441,348 +151,3 @@ def __repr__(self): else: return "TextureSprite(format=%d, access=%d, size=%s, angle=%f)" % \ (flags.value, access.value, (w.value, h.value), self.angle) - -class SpriteFactory(object): - """A factory class for creating Sprite components.""" - def __init__(self, sprite_type=TEXTURE, **kwargs): - """Creates a new SpriteFactory. - - The SpriteFactory can create TextureSprite or SoftwareSprite - instances, depending on the sprite_type being passed to it, - which can be SOFTWARE or TEXTURE. The additional kwargs are used - as default arguments for creating sprites within the factory - methods. - """ - if sprite_type == TEXTURE: - if "renderer" not in kwargs: - raise ValueError("you have to provide a renderer= argument") - elif sprite_type != SOFTWARE: - raise ValueError("sprite_type must be TEXTURE or SOFTWARE") - self._spritetype = sprite_type - self.default_args = kwargs - - @property - def sprite_type(self): - """The sprite type created by the factory.""" - return self._spritetype - - def __repr__(self): - stype = "TEXTURE" - if self.sprite_type == SOFTWARE: - stype = "SOFTWARE" - return "SpriteFactory(sprite_type=%s, default_args=%s)" % \ - (stype, self.default_args) - - def create_sprite_render_system(self, *args, **kwargs): - """Creates a new SpriteRenderSystem. - - For TEXTURE mode, the passed args and kwargs are ignored and the - Renderer or SDL_Renderer passed to the SpriteFactory is used. - """ - if self.sprite_type == TEXTURE: - return TextureSpriteRenderSystem(self.default_args["renderer"]) - else: - return SoftwareSpriteRenderSystem(*args, **kwargs) - - def from_image(self, fname): - """Creates a Sprite from the passed image file.""" - return self.from_surface(load_image(fname), True) - - def from_surface(self, tsurface, free=False): - """Creates a Sprite from the passed SDL_Surface. - - If free is set to True, the passed surface will be freed - automatically. - """ - if self.sprite_type == TEXTURE: - renderer = self.default_args["renderer"] - texture = render.SDL_CreateTextureFromSurface(renderer.sdlrenderer, - tsurface) - if not texture: - raise SDLError() - sprite = TextureSprite(texture.contents) - if free: - surface.SDL_FreeSurface(tsurface) - return sprite - elif self.sprite_type == SOFTWARE: - return SoftwareSprite(tsurface, free) - raise ValueError("sprite_type must be TEXTURE or SOFTWARE") - - def from_object(self, obj): - """Creates a Sprite from an arbitrary object.""" - if self.sprite_type == TEXTURE: - rw = rwops.rw_from_object(obj) - # TODO: support arbitrary objects. - imgsurface = surface.SDL_LoadBMP_RW(rw, True) - if not imgsurface: - raise SDLError() - return self.from_surface(imgsurface.contents, True) - elif self.sprite_type == SOFTWARE: - rw = rwops.rw_from_object(obj) - imgsurface = surface.SDL_LoadBMP_RW(rw, True) - if not imgsurface: - raise SDLError() - return SoftwareSprite(imgsurface.contents, True) - raise ValueError("sprite_type must be TEXTURE or SOFTWARE") - - def from_color(self, color, size, bpp=32, masks=None): - """Creates a sprite with a certain color. - """ - color = convert_to_color(color) - if masks: - rmask, gmask, bmask, amask = masks - else: - rmask = gmask = bmask = amask = 0 - sfc = surface.SDL_CreateRGBSurface(0, size[0], size[1], bpp, rmask, - gmask, bmask, amask) - if not sfc: - raise SDLError() - fmt = sfc.contents.format - if fmt.contents.Amask != 0: - # Target has an alpha mask - col = pixels.SDL_MapRGBA(fmt, color.r, color.g, color.b, color.a) - else: - col = pixels.SDL_MapRGB(fmt, color.r, color.g, color.b) - ret = surface.SDL_FillRect(sfc, None, col) - if ret == -1: - raise SDLError() - return self.from_surface(sfc.contents, True) - - def from_text(self, text, **kwargs): - """Creates a Sprite from a string of text.""" - args = self.default_args.copy() - args.update(kwargs) - fontmanager = args['fontmanager'] - sfc = fontmanager.render(text, **args) - return self.from_surface(sfc, free=True) - - def create_sprite(self, **kwargs): - """Creates an empty Sprite. - - This will invoke create_software_sprite() or - create_texture_sprite() with the passed arguments and the set - default arguments. - """ - args = self.default_args.copy() - args.update(kwargs) - if self.sprite_type == TEXTURE: - return self.create_texture_sprite(**args) - else: - return self.create_software_sprite(**args) - - def create_software_sprite(self, size, bpp=32, masks=None): - """Creates a software sprite. - - A size tuple containing the width and height of the sprite and a - bpp value, indicating the bits per pixel to be used, need to be - provided. - """ - if masks: - rmask, gmask, bmask, amask = masks - else: - rmask = gmask = bmask = amask = 0 - imgsurface = surface.SDL_CreateRGBSurface(0, size[0], size[1], bpp, - rmask, gmask, bmask, amask) - if not imgsurface: - raise SDLError() - return SoftwareSprite(imgsurface.contents, True) - - def create_texture_sprite(self, renderer, size, - pformat=pixels.SDL_PIXELFORMAT_RGBA8888, - access=render.SDL_TEXTUREACCESS_STATIC): - """Creates a texture sprite. - - A size tuple containing the width and height of the sprite needs - to be provided. - - TextureSprite objects are assumed to be static by default, - making it impossible to access their pixel buffer in favour for - faster copy operations. If you need to update the pixel data - frequently or want to use the texture as target for rendering - operations, access can be set to the relevant - SDL_TEXTUREACCESS_* flag. - """ - if isinstance(renderer, render.SDL_Renderer): - sdlrenderer = renderer - elif isinstance(renderer, Renderer): - sdlrenderer = renderer.sdlrenderer - else: - raise TypeError("renderer must be a Renderer or SDL_Renderer") - texture = render.SDL_CreateTexture(sdlrenderer, pformat, access, - size[0], size[1]) - if not texture: - raise SDLError() - return TextureSprite(texture.contents) - - -class SpriteRenderSystem(System): - """A rendering system for Sprite components. - - This is a base class for rendering systems capable of drawing and - displaying Sprite-based objects. Inheriting classes need to - implement the rendering capability by overriding the render() - method. - """ - def __init__(self): - super(SpriteRenderSystem, self).__init__() - self.componenttypes = (Sprite,) - self._sortfunc = lambda e: e.depth - - def render(self, sprites, x=None, y=None): - """Renders the passed sprites. - - This is a no-op function and needs to be implemented by inheriting - classes. - """ - pass - - def process(self, world, components): - """Draws the passed SoftSprite objects on the Window's surface.""" - self.render(sorted(components, key=self._sortfunc)) - - @property - def sortfunc(self): - """Sort function for the component processing order. - - The default sort order is based on the depth attribute of every - sprite. Lower depth values will cause sprites to be drawn below - sprites with higher depth values. - """ - return self._sortfunc - - @sortfunc.setter - def sortfunc(self, value): - """Sort function for the component processing order. - - The default sort order is based on the depth attribute of every - sprite. Lower depth values will cause sprites to be drawn below - sprites with higher depth values. - """ - if not callable(value): - raise TypeError("sortfunc must be callable") - self._sortfunc = value - - -class SoftwareSpriteRenderSystem(SpriteRenderSystem): - """A rendering system for SoftwareSprite components. - - The SoftwareSpriteRenderSystem class uses a Window as drawing device to - display Sprite surfaces. It uses the Window's internal SDL surface as - drawing context, so that GL operations, such as texture handling or - using SDL renderers is not possible. - """ - def __init__(self, window): - """Creates a new SoftwareSpriteRenderSystem for a specific Window.""" - super(SoftwareSpriteRenderSystem, self).__init__() - if isinstance(window, Window): - self.window = window.window - elif isinstance(window, video.SDL_Window): - self.window = window - else: - raise TypeError("unsupported window type") - self.target = window - sfc = video.SDL_GetWindowSurface(self.window) - if not sfc: - raise SDLError() - self.surface = sfc.contents - self.componenttypes = (SoftwareSprite,) - - def render(self, sprites, x=None, y=None): - """Draws the passed sprites (or sprite) on the Window's surface. - - x and y are optional arguments that can be used as relative drawing - location for sprites. If set to None, the location information of the - sprites are used. If set and sprites is an iterable, such as a list of - SoftwareSprite objects, x and y are relative location values that will - be added to each individual sprite's position. If sprites is a single - SoftwareSprite, x and y denote the absolute position of the - SoftwareSprite, if set. - """ - r = rect.SDL_Rect(0, 0, 0, 0) - if isiterable(sprites): - blit_surface = surface.SDL_BlitSurface - imgsurface = self.surface - x = x or 0 - y = y or 0 - for sprite in sprites: - r.x = x + sprite.x - r.y = y + sprite.y - blit_surface(sprite.surface, None, imgsurface, r) - else: - r.x = sprites.x - r.y = sprites.y - if x is not None and y is not None: - r.x = x - r.y = y - surface.SDL_BlitSurface(sprites.surface, None, self.surface, r) - video.SDL_UpdateWindowSurface(self.window) - - -class TextureSpriteRenderSystem(SpriteRenderSystem): - """A rendering system for TextureSprite components. - - The TextureSpriteRenderSystem class uses a SDL_Renderer as drawing - device to display TextureSprite objects. - """ - def __init__(self, target): - """Creates a new TextureSpriteRenderSystem. - - target can be a Window, SDL_Window, Renderer or SDL_Renderer. - If it is a Window or SDL_Window instance, a Renderer will be - created to acquire the SDL_Renderer. - """ - super(TextureSpriteRenderSystem, self).__init__() - if isinstance(target, (Window, video.SDL_Window)): - # Create a Renderer for the window and use that one. - target = Renderer(target) - - if isinstance(target, Renderer): - self._renderer = target # Used to prevent GC - sdlrenderer = target.sdlrenderer - elif isinstance(target, render.SDL_Renderer): - sdlrenderer = target - else: - raise TypeError("unsupported object type") - self.sdlrenderer = sdlrenderer - self.componenttypes = (TextureSprite,) - - def __del__(self): - self.sdlrenderer = None - if hasattr(self, "_renderer"): - self._renderer = None - - def render(self, sprites, x=None, y=None): - """Draws the passed sprites (or sprite). - - x and y are optional arguments that can be used as relative - drawing location for sprites. If set to None, the location - information of the sprites are used. If set and sprites is an - iterable, such as a list of TextureSprite objects, x and y are - relative location values that will be added to each individual - sprite's position. If sprites is a single TextureSprite, x and y - denote the absolute position of the TextureSprite, if set. - """ - r = rect.SDL_Rect(0, 0, 0, 0) - rcopy = render.SDL_RenderCopyEx - if isiterable(sprites): - renderer = self.sdlrenderer - x = x or 0 - y = y or 0 - for sprite in sprites: - r.x = x + sprite.x - r.y = y + sprite.y - r.w, r.h = sprite.size - if rcopy(renderer, sprite.texture, None, r, sprite.angle, - sprite.center, sprite.flip) == -1: - raise SDLError() - else: - r.x = sprites.x - r.y = sprites.y - r.w, r.h = sprites.size - if x is not None and y is not None: - r.x = x - r.y = y - if rcopy(self.sdlrenderer, sprites.texture, None, r, sprites.angle, - sprites.center, sprites.flip) == -1: - raise SDLError() - render.SDL_RenderPresent(self.sdlrenderer) diff --git a/sdl2/ext/spritesystem.py b/sdl2/ext/spritesystem.py new file mode 100644 index 00000000..34c087f9 --- /dev/null +++ b/sdl2/ext/spritesystem.py @@ -0,0 +1,365 @@ +from .. import surface, rect, video, pixels, render, rwops + +from .color import convert_to_color +from .common import SDLError +from .compat import isiterable +from .ebs import System +from .image import load_image +from .renderer import Renderer +from .sprite import Sprite, SoftwareSprite, TextureSprite +from .window import Window + +__all__ = [ + "SpriteFactory", "SoftwareSpriteRenderSystem", "SpriteRenderSystem", + "TextureSpriteRenderSystem", "TEXTURE", "SOFTWARE" +] + + +TEXTURE = 0 +SOFTWARE = 1 + + +class SpriteFactory(object): + """A factory class for creating Sprite components.""" + def __init__(self, sprite_type=TEXTURE, **kwargs): + """Creates a new SpriteFactory. + + The SpriteFactory can create TextureSprite or SoftwareSprite + instances, depending on the sprite_type being passed to it, + which can be SOFTWARE or TEXTURE. The additional kwargs are used + as default arguments for creating sprites within the factory + methods. + """ + if sprite_type == TEXTURE: + if "renderer" not in kwargs: + raise ValueError("you have to provide a renderer= argument") + elif sprite_type != SOFTWARE: + raise ValueError("sprite_type must be TEXTURE or SOFTWARE") + self._spritetype = sprite_type + self.default_args = kwargs + + @property + def sprite_type(self): + """The sprite type created by the factory.""" + return self._spritetype + + def __repr__(self): + stype = "TEXTURE" + if self.sprite_type == SOFTWARE: + stype = "SOFTWARE" + return "SpriteFactory(sprite_type=%s, default_args=%s)" % \ + (stype, self.default_args) + + def create_sprite_render_system(self, *args, **kwargs): + """Creates a new SpriteRenderSystem. + + For TEXTURE mode, the passed args and kwargs are ignored and the + Renderer or SDL_Renderer passed to the SpriteFactory is used. + """ + if self.sprite_type == TEXTURE: + return TextureSpriteRenderSystem(self.default_args["renderer"]) + else: + return SoftwareSpriteRenderSystem(*args, **kwargs) + + def from_image(self, fname): + """Creates a Sprite from the passed image file.""" + return self.from_surface(load_image(fname), True) + + def from_surface(self, tsurface, free=False): + """Creates a Sprite from the passed SDL_Surface. + + If free is set to True, the passed surface will be freed + automatically. + """ + if self.sprite_type == TEXTURE: + renderer = self.default_args["renderer"] + texture = render.SDL_CreateTextureFromSurface(renderer.sdlrenderer, + tsurface) + if not texture: + raise SDLError() + sprite = TextureSprite(texture.contents) + if free: + surface.SDL_FreeSurface(tsurface) + return sprite + elif self.sprite_type == SOFTWARE: + return SoftwareSprite(tsurface, free) + raise ValueError("sprite_type must be TEXTURE or SOFTWARE") + + def from_object(self, obj): + """Creates a Sprite from an arbitrary object.""" + if self.sprite_type == TEXTURE: + rw = rwops.rw_from_object(obj) + # TODO: support arbitrary objects. + imgsurface = surface.SDL_LoadBMP_RW(rw, True) + if not imgsurface: + raise SDLError() + return self.from_surface(imgsurface.contents, True) + elif self.sprite_type == SOFTWARE: + rw = rwops.rw_from_object(obj) + imgsurface = surface.SDL_LoadBMP_RW(rw, True) + if not imgsurface: + raise SDLError() + return SoftwareSprite(imgsurface.contents, True) + raise ValueError("sprite_type must be TEXTURE or SOFTWARE") + + def from_color(self, color, size, bpp=32, masks=None): + """Creates a sprite with a certain color. + """ + color = convert_to_color(color) + if masks: + rmask, gmask, bmask, amask = masks + else: + rmask = gmask = bmask = amask = 0 + sfc = surface.SDL_CreateRGBSurface(0, size[0], size[1], bpp, rmask, + gmask, bmask, amask) + if not sfc: + raise SDLError() + fmt = sfc.contents.format + if fmt.contents.Amask != 0: + # Target has an alpha mask + col = pixels.SDL_MapRGBA(fmt, color.r, color.g, color.b, color.a) + else: + col = pixels.SDL_MapRGB(fmt, color.r, color.g, color.b) + ret = surface.SDL_FillRect(sfc, None, col) + if ret == -1: + raise SDLError() + return self.from_surface(sfc.contents, True) + + def from_text(self, text, **kwargs): + """Creates a Sprite from a string of text.""" + args = self.default_args.copy() + args.update(kwargs) + fontmanager = args['fontmanager'] + sfc = fontmanager.render(text, **args) + return self.from_surface(sfc, free=True) + + def create_sprite(self, **kwargs): + """Creates an empty Sprite. + + This will invoke create_software_sprite() or + create_texture_sprite() with the passed arguments and the set + default arguments. + """ + args = self.default_args.copy() + args.update(kwargs) + if self.sprite_type == TEXTURE: + return self.create_texture_sprite(**args) + else: + return self.create_software_sprite(**args) + + def create_software_sprite(self, size, bpp=32, masks=None): + """Creates a software sprite. + + A size tuple containing the width and height of the sprite and a + bpp value, indicating the bits per pixel to be used, need to be + provided. + """ + if masks: + rmask, gmask, bmask, amask = masks + else: + rmask = gmask = bmask = amask = 0 + imgsurface = surface.SDL_CreateRGBSurface(0, size[0], size[1], bpp, + rmask, gmask, bmask, amask) + if not imgsurface: + raise SDLError() + return SoftwareSprite(imgsurface.contents, True) + + def create_texture_sprite(self, renderer, size, + pformat=pixels.SDL_PIXELFORMAT_RGBA8888, + access=render.SDL_TEXTUREACCESS_STATIC): + """Creates a texture sprite. + + A size tuple containing the width and height of the sprite needs + to be provided. + + TextureSprite objects are assumed to be static by default, + making it impossible to access their pixel buffer in favour for + faster copy operations. If you need to update the pixel data + frequently or want to use the texture as target for rendering + operations, access can be set to the relevant + SDL_TEXTUREACCESS_* flag. + """ + if isinstance(renderer, render.SDL_Renderer): + sdlrenderer = renderer + elif isinstance(renderer, Renderer): + sdlrenderer = renderer.sdlrenderer + else: + raise TypeError("renderer must be a Renderer or SDL_Renderer") + texture = render.SDL_CreateTexture(sdlrenderer, pformat, access, + size[0], size[1]) + if not texture: + raise SDLError() + return TextureSprite(texture.contents) + + +class SpriteRenderSystem(System): + """A rendering system for Sprite components. + + This is a base class for rendering systems capable of drawing and + displaying Sprite-based objects. Inheriting classes need to + implement the rendering capability by overriding the render() + method. + """ + def __init__(self): + super(SpriteRenderSystem, self).__init__() + self.componenttypes = (Sprite,) + self._sortfunc = lambda e: e.depth + + def render(self, sprites, x=None, y=None): + """Renders the passed sprites. + + This is a no-op function and needs to be implemented by inheriting + classes. + """ + pass + + def process(self, world, components): + """Draws the passed SoftSprite objects on the Window's surface.""" + self.render(sorted(components, key=self._sortfunc)) + + @property + def sortfunc(self): + """Sort function for the component processing order. + + The default sort order is based on the depth attribute of every + sprite. Lower depth values will cause sprites to be drawn below + sprites with higher depth values. + """ + return self._sortfunc + + @sortfunc.setter + def sortfunc(self, value): + """Sort function for the component processing order. + + The default sort order is based on the depth attribute of every + sprite. Lower depth values will cause sprites to be drawn below + sprites with higher depth values. + """ + if not callable(value): + raise TypeError("sortfunc must be callable") + self._sortfunc = value + + +class SoftwareSpriteRenderSystem(SpriteRenderSystem): + """A rendering system for SoftwareSprite components. + + The SoftwareSpriteRenderSystem class uses a Window as drawing device to + display Sprite surfaces. It uses the Window's internal SDL surface as + drawing context, so that GL operations, such as texture handling or + using SDL renderers is not possible. + """ + def __init__(self, window): + """Creates a new SoftwareSpriteRenderSystem for a specific Window.""" + super(SoftwareSpriteRenderSystem, self).__init__() + if isinstance(window, Window): + self.window = window.window + elif isinstance(window, video.SDL_Window): + self.window = window + else: + raise TypeError("unsupported window type") + self.target = window + sfc = video.SDL_GetWindowSurface(self.window) + if not sfc: + raise SDLError() + self.surface = sfc.contents + self.componenttypes = (SoftwareSprite,) + + def render(self, sprites, x=None, y=None): + """Draws the passed sprites (or sprite) on the Window's surface. + + x and y are optional arguments that can be used as relative drawing + location for sprites. If set to None, the location information of the + sprites are used. If set and sprites is an iterable, such as a list of + SoftwareSprite objects, x and y are relative location values that will + be added to each individual sprite's position. If sprites is a single + SoftwareSprite, x and y denote the absolute position of the + SoftwareSprite, if set. + """ + r = rect.SDL_Rect(0, 0, 0, 0) + if isiterable(sprites): + blit_surface = surface.SDL_BlitSurface + imgsurface = self.surface + x = x or 0 + y = y or 0 + for sprite in sprites: + r.x = x + sprite.x + r.y = y + sprite.y + blit_surface(sprite.surface, None, imgsurface, r) + else: + r.x = sprites.x + r.y = sprites.y + if x is not None and y is not None: + r.x = x + r.y = y + surface.SDL_BlitSurface(sprites.surface, None, self.surface, r) + video.SDL_UpdateWindowSurface(self.window) + + +class TextureSpriteRenderSystem(SpriteRenderSystem): + """A rendering system for TextureSprite components. + + The TextureSpriteRenderSystem class uses a SDL_Renderer as drawing + device to display TextureSprite objects. + """ + def __init__(self, target): + """Creates a new TextureSpriteRenderSystem. + + target can be a Window, SDL_Window, Renderer or SDL_Renderer. + If it is a Window or SDL_Window instance, a Renderer will be + created to acquire the SDL_Renderer. + """ + super(TextureSpriteRenderSystem, self).__init__() + if isinstance(target, (Window, video.SDL_Window)): + # Create a Renderer for the window and use that one. + target = Renderer(target) + + if isinstance(target, Renderer): + self._renderer = target # Used to prevent GC + sdlrenderer = target.sdlrenderer + elif isinstance(target, render.SDL_Renderer): + sdlrenderer = target + else: + raise TypeError("unsupported object type") + self.sdlrenderer = sdlrenderer + self.componenttypes = (TextureSprite,) + + def __del__(self): + self.sdlrenderer = None + if hasattr(self, "_renderer"): + self._renderer = None + + def render(self, sprites, x=None, y=None): + """Draws the passed sprites (or sprite). + + x and y are optional arguments that can be used as relative + drawing location for sprites. If set to None, the location + information of the sprites are used. If set and sprites is an + iterable, such as a list of TextureSprite objects, x and y are + relative location values that will be added to each individual + sprite's position. If sprites is a single TextureSprite, x and y + denote the absolute position of the TextureSprite, if set. + """ + r = rect.SDL_Rect(0, 0, 0, 0) + rcopy = render.SDL_RenderCopyEx + if isiterable(sprites): + renderer = self.sdlrenderer + x = x or 0 + y = y or 0 + for sprite in sprites: + r.x = x + sprite.x + r.y = y + sprite.y + r.w, r.h = sprite.size + if rcopy(renderer, sprite.texture, None, r, sprite.angle, + sprite.center, sprite.flip) == -1: + raise SDLError() + else: + r.x = sprites.x + r.y = sprites.y + r.w, r.h = sprites.size + if x is not None and y is not None: + r.x = x + r.y = y + if rcopy(self.sdlrenderer, sprites.texture, None, r, sprites.angle, + sprites.center, sprites.flip) == -1: + raise SDLError() + render.SDL_RenderPresent(self.sdlrenderer) diff --git a/sdl2/ext/surface.py b/sdl2/ext/surface.py index 452742a9..667c35cf 100644 --- a/sdl2/ext/surface.py +++ b/sdl2/ext/surface.py @@ -1,19 +1,114 @@ -"""Surface manipulation.""" -from ..surface import SDL_CreateRGBSurfaceFrom +from .compat import isiterable +from .common import raise_sdl_err +from .color import convert_to_color +from .. import pixels +from .. import surface as surf +from ..rect import SDL_Rect __all__ = ["subsurface"] + +def _get_rect_tuple(r, argname): + if isinstance(r, SDL_Rect): + return (r.x, r.y, r.w, r.h) + elif isiterable(r) and len(r) == 4: + return tuple(r) + else: + e = "'{0}' must be an SDL_Rect or tuple of 4 integers." + raise TypeError(e.format(argname)) + + +def _get_target_surface(target, argname="target"): + """Gets the SDL_surface from the passed target.""" + if hasattr(target, "surface"): # i.e. if SoftwareSprite + rtarget = target.surface + elif isinstance(target, surf.SDL_Surface): + rtarget = target + elif "SDL_Surface" in str(type(target)): + rtarget = target.contents + else: + raise TypeError( + "{0} must be a valid Sprite or SDL Surface".format(argname) + ) + return rtarget + + +def _create_surface(size, fill=None, fmt="ARGB8888", errname="SDL"): + # Perform initial type and argument checking + if not isiterable(size) and len(size) == 2: + e = "Surface size must be a tuple of two positive integers." + raise TypeError(e) + if not all([i > 0 and int(i) == i for i in size]): + e = "Surface height and width must both be positive integers (got {0})." + raise ValueError(e.format(str(size))) + if fmt not in pixels.NAME_MAP.keys() and fmt not in pixels.ALL_PIXELFORMATS: + e = "'{0}' is not a supported SDL pixel format." + raise ValueError(e.format(fmt)) + if fill is not None: + fill = convert_to_color(fill) + + # Actually create a surface with the given pixel format + w, h = size + bpp = 32 # NOTE: according to the SDL_surface.c code, this has no effect + fmt_enum = fmt if type(fmt) == int else pixels.NAME_MAP[fmt] + sf = surf.SDL_CreateRGBSurfaceWithFormat(0, w, h, bpp, fmt_enum) + if not sf: + raise_sdl_err("creating the {0} surface".format(errname)) + + # If provided, set the fill for the new surface + if fill is not None: + pixfmt = sf.contents.format.contents + if pixfmt.Amask == 0: + fill_col = pixels.SDL_MapRGB(pixfmt, fill.r, fill.g, fill.b) + else: + fill_col = pixels.SDL_MapRGBA(pixfmt, fill.r, fill.g, fill.b, fill.a) + surf.SDL_FillRect(sf, None, fill_col) + + return sf + + def subsurface(surface, area): - """Creates a surface from a part of another surface. + """Creates a new :obj:`~sdl2.SDL_Surface` from a part of another surface. + + Surfaces created with this function will share pixel data with the original + surface, meaning that any modifications to one surface will result in + modifications to the other. + + .. warning:: + Because subsurfaces share pixel data with their parent surface, they + *cannot* be used after the parent surface is freed. Doing so will + almost certainly result in a segfault. + + Args: + surface (:obj:`~sdl2.SDL_Surface`): The parent surface from which + new sub-surface should be created. + area (:obj:`SDL_Rect`, tuple): The ``(x, y, w, h)`` subset of the parent + surface to use for the new surface, where ``x, y`` are the pixel + coordinates of the top-left corner of the rectangle and ``w, h`` are + its width and height (in pixels). Can also be specified as an + :obj:`SDL_Rect`. + + Returns: + :obj:`~sdl2.SDL_Surface`: The newly-created subsurface. - The two surfaces share pixel data. The subsurface *must not* be used after - its parent has been freed! """ - surface_format = surface.format[0] - subpixels = (surface.pixels + surface.pitch*area[1] + - surface_format.BytesPerPixel*area[0]) - return SDL_CreateRGBSurfaceFrom(subpixels, area[2], area[3], - surface_format.BitsPerPixel, - surface.pitch, surface_format.Rmask, - surface_format.Gmask, surface_format.Bmask, - surface_format.Amask)[0] + if not isinstance(surface, surf.SDL_Surface): + if "SDL_Surface" in str(type(surface)): + surface = surface.contents + else: + e = "'surface' must be an SDL_Surface (got {0})" + raise TypeError(e.format(type(surface))) + + x, y, w, h = _get_rect_tuple(area, argname="area") + if x + w > surface.w or y + h > surface.h: + e = "The specified area {0} exceeds the bounds of the parent surface " + e += str((surface.w, surface.h)) + raise ValueError(e.format(str(area))) + + fmt = surface.format[0] + bpp = fmt.BitsPerPixel + subpixels = (surface.pixels + surface.pitch * y + fmt.BytesPerPixel * x) + new = surf.SDL_CreateRGBSurfaceFrom( + subpixels, w, h, bpp, surface.pitch, fmt.Rmask, fmt.Gmask, fmt.Bmask, fmt.Amask + ) + return new.contents diff --git a/sdl2/ext/ttf.py b/sdl2/ext/ttf.py new file mode 100644 index 00000000..79f30a12 --- /dev/null +++ b/sdl2/ext/ttf.py @@ -0,0 +1,702 @@ +import os +import re +from ctypes import c_int, byref +from .. import surface, pixels, rwops, error, rect +from .common import SDLError, raise_sdl_err +from .compat import * +from .color import Color, convert_to_color +from .draw import prepare_color +from .resources import _validate_path +from .surface import _create_surface + +_HASSDLTTF = True +try: + from .. import sdlttf +except ImportError: + _HASSDLTTF = False + +__all__ = ["FontTTF", "FontManager"] + + +def _ttf_init(): + if not _HASSDLTTF: + raise RuntimeError("SDL_ttf is required, but is not installed.") + + # Check if TTF already initialized, return immediately if it was + if sdlttf.TTF_WasInit() > 0: + return + + # Handle a weirdness in how TTF_Init and TTF_Quit work: TTF_Init + # blindly increments TTF_WasInit every time it's called and TTF_Quit + # blindly decrements it, but TTF_Quit only *actually* quits when + # TTF_WasInit - 1 == 0. Here, we try to ensure we're starting at 0. + while sdlttf.TTF_WasInit() < 1: + ret = sdlttf.TTF_Init() + if ret != 0: + raise_sdl_err("initializing the SDL_ttf library") + + +def _ttf_quit(): + if not _HASSDLTTF: + raise RuntimeError("SDL_ttf is required, but is not installed.") + + # Make sure WasInit is non-negative before trying to quit + while sdlttf.TTF_WasInit() < 1: + ret = sdlttf.TTF_Init() + if ret != 0: + raise_sdl_err("initializing the SDL_ttf library") + + # Actually quit the library (won't really quit until TTF_WasInit == 0) + while sdlttf.TTF_WasInit > 0: + sdlttf.TTF_Quit() + + +def _is_whitespace(s): + ws_chars = [" ", "\n", "\t", "\r"] + return len(s) == 0 or all([c in ws_chars for c in s]) + + +def _split_on_whitespace(line): + # Splits a string on whitespace starts without removing the whitespace + words = [] + current_word = "" + for s in re.split(r'(\s+)', line): + current_word += s + if not _is_whitespace(s): + words.append(current_word) + current_word = "" + return words + + + +class FontTTF(object): + """A class for rendering text with a given TrueType font. + + This class loads a TrueType or OpenType font using the **SDL_ttf** library + and allows the user to render text with it in various sizes and colors. + + To simplify rendering text with different font sizes and colors, this class + allows users to define font styles (e.g. 'instructions', 'error', 'title', + etc.) using :meth:`add_style`, which can then be used by name with the + :meth:`render_text` method. + + By default, the :class:`FontTTF` class defines font sizes in points (pt), a + common unit of font size used by many libraries and text editors. However, + you can also define font sizes in units of *pixel height* (px), i.e. the + ascent height of the font's tallest alphanumeric ASCII character. This is + done by calculating each font's px-to-pt scaling factor on import, meaning + that some rounding error may occur. If using a font that does not include + all alphanumeric ASCII characters (A-Z, a-z, 0-9) or wanting to scale the + maximum pixel height to a different subset of characters (e.g. just 0-9 + digits), you will need to specifiy a custom 'height_chars' string when + creating the font object. + + Args: + path (str): The relative (or absolute) path to the font to load. + size (int or str): The default size for the font, either as an integer + (assumed to be in pt) or as a string specifying the unit of size + (e.g. '12pt' or '22px'). + color (~sdl2.ext.Color or tuple): The default color to use for + rendering text. Defaults to white if not specified. + index (int, optional): The index of the font face to load if the font + file contains multiple faces. Defaults to 0 (first face in the file) + if not specified. + height_chars (str, optional): The set of font characters to use for + calculating the maximum height (in pixels) of the font for + + """ + def __init__(self, path, size, color, index=0, height_chars=None): + fullpath, fname = _validate_path(path, "a font") + fullpath = byteify(fullpath) + _ttf_init() + + # Load the font data as an RWops object for fast repeat opening + self._font_rw = rwops.rw_from_object(open(fullpath, "rb")) + self._index = index + + # Get the px-to-pt scaling factor for the font + if not height_chars: + caps = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + height_chars = caps + caps.lower() + "0123456789" + self._scale_factor = self._get_scale_factor(height_chars, fname) + + # Initialize font styles and add the default style + self._styles = {} + self.add_style("default", size, color) + + def _check_if_closed(self): + # Makes sure the font hasn't been closed, raising an exception if it has + if self._font_rw == None: + e = "The font has been closed and can no longer be used." + raise RuntimeError(e) + + def _get_scale_factor(self, testchars, fname): + # Calculate the pt -> px conversion factor for the current font. + tmpsize = 40 # Font size in pt at which to calculate pt -> px scale factor + max_ascent = 0 + minX, maxX, minY, maxY = c_int(0), c_int(0), c_int(0), c_int(0) + advance = c_int(0) + tmpfont = sdlttf.TTF_OpenFontIndexRW(self._font_rw, 0, tmpsize, self._index) + if not tmpfont: + raise_sdl_err("initializing the font '{0}'".format(fname)) + for char in testchars: + if sdlttf.TTF_GlyphIsProvided(tmpfont, ord(char)) == 0: + e = "The current font does not provide a glyph for character " + e += "'{0}'. Please initialize the font with a different " + e += "'height_chars' string." + sdlttf.TTF_CloseFont(tmpfont) + raise RuntimeError(e.format(char.encode('utf-8'))) + sdlttf.TTF_GlyphMetrics( + tmpfont, ord(char), byref(minX), byref(maxX), + byref(minY), byref(maxY), byref(advance) + ) + if maxY.value > max_ascent: + max_ascent = maxY.value + sdlttf.TTF_CloseFont(tmpfont) + return tmpsize / float(max_ascent) + + def _parse_size(self, size): + # Parse and validate a font size, which can be an int or a string + if isinstance(size, str): + if '-' in size or '.' in size: + e = "Font size must be a positive whole number (got '{0}')." + raise ValueError(e.format(size)) + sz = "".join([i for i in size if i.isdigit()]) + unit = size[len(sz):] + sz = int(sz) + if unit == "px": + sz = int(sz * self._scale_factor) + elif unit not in ["pt", "px"]: + e = "Font size units must be either 'pt' or 'px' (got '{0}')." + raise ValueError(e.format(unit)) + else: + sz = int(size) + return sz + + def _get_line_size(self, line, style): + # Get the height and width of a given line of text in a given style + font = self._styles[style]['font'] + if len(line) == 0: + return (0, sdlttf.TTF_FontHeight(font)) + lw, lh = c_int(0), c_int(0) + ret = sdlttf.TTF_SizeUTF8(font, byteify(line), byref(lw), byref(lh)) + if ret != 0: + raise_sdl_err("rendering text with the '{0}' style".format(style)) + return (lw.value, lh.value) + + def _wrap_line(self, line, style, line_width, width): + # Splits a single line of text into separate lines in order to fit + # within a given width with a given text style + lines = [] + while line_width > width: + + # First, estimate the position of the wrap word and move to the + # end of it + pos = int((width / float(line_width)) * len(line)) + words = _split_on_whitespace(line) + for i in range(len(words)): + split_word = i + 1 + segment = "".join(words[:split_word]) + if len(segment) >= pos and len(segment) < len(line): + pos = len(segment) + break + + # Then, work backwards until the line fits within the width + segment_w = self._get_line_size(segment, style)[0] + while segment_w > width: + if split_word > 1: + split_word -= 1 + segment = "".join(words[:split_word]) + pos = len(segment) + elif pos > 1: + pos -= 1 + segment = segment[:pos] + else: + e = "Character '{0}' exceeds maximum width of text" + raise RuntimeError(e.format(line[0])) + segment_w = self._get_line_size(segment, style)[0] + + # Append the trimmed line & remove the text from the line, then + # recalculate the width of the remaining text to see if it fits + lines.append(segment) + line = line[pos:].lstrip() + line_width = self._get_line_size(line, style)[0] + + lines.append(line) + return lines + + def _split_lines(self, text, style, width=None): + # Splits one or more lines of text to ensure they all fit within a given + # width (in pixels) when rendered with a given text style + unwrapped_lines = text.split("\n") + if width is None: + return unwrapped_lines + + lines = [] + for line in unwrapped_lines: + lw = self._get_line_size(line, style)[0] + if lw > width: + # If line is too long and contains trailing whitespace, try w/o + if len(line) < len(line.rstrip()): + line = line.rstrip() + lw = self._get_line_size(line, style)[0] + # If line width exceeds wrap width, we break it into smaller lines + lines += self._wrap_line(line, style, lw, width) + else: + lines.append(line) + + return lines + + def _render_lines(self, lines, style, line_h, width, align): + # Renders one or more lines of text to an SDL surface, optionally with + # a custom line height, wrap width, and/or text alignment + font = self._styles[style]['font'] + color = self._styles[style]['color'] + bg_col = self._styles[style]['bg'] + if not bg_col: + c = color + bg_col = (c.r, c.g, c.b, 0) # Default bg = font color w/ full transparency + + # Actually render the text + rendered = [] + for line in lines: + fontsf = sdlttf.TTF_RenderUTF8_Blended(font, byteify(line), color) + if not fontsf: + raise_sdl_err("rendering text with the '{0}' style".format(style)) + rendered.append(fontsf) + + # If single line of text w/ no background fill or custom width, return as-is + if len(rendered) == 1 and width == None and bg_col is None: + return rendered[0] + + # Determine height and width of background surface + font_height = sdlttf.TTF_FontHeight(font) + line_h = sdlttf.TTF_FontLineSkip(font) if not line_h else line_h + height = line_h * (len(lines) - 1) + font_height + if width == None: + width = max([line.contents.w for line in rendered]) + + # Create background surface for the text and render lines to it + sf = _create_surface((width, height), bg_col, errname="background") + line_y = 0 + for line in rendered: + lw, lh = (line.contents.w, line.contents.h) + if align == "left": + line_x = 0 + elif align == "center": + line_x = int((width - lw) / 2) + elif align == "right": + line_x = width - lw + line_rect = rect.SDL_Rect(line_x, line_y, lw, lh) + surface.SDL_BlitSurface(line, None, sf, line_rect) + surface.SDL_FreeSurface(line) + line_y += line_h + + return sf + + def get_ttf_font(self, style='default'): + """Returns the base :obj:`~sdl2.sdlttf.TTF_Font` for a given font style. + + This method provides access to the base ctypes object for each font + style so that they can be used with the full set of :mod:`~sdl2.sdlttf` + functions. + + Args: + style (str, optional): The font style for which the base ctypes font + object will be retrieved. Defaults to the 'default' style if not + specified. + + Returns: + :obj:`~sdl2.sdlttf.TTF_Font`: The base ctypes font for the style. + + """ + self._check_if_closed() + if style not in self._styles.keys(): + e = "The '{0}' style is not defined for the current font." + raise ValueError(e.format(style)) + return self._styles[style]['font'].contents + + def add_style(self, name, size, color, bg_color=None): + """Defines a named font style for the current font. + + Currently, a font style defines a font size, color, and background color + combination to use for rendering text. Additional style attributes may + be added in future releases. + + Args: + name (str): The name of the new font style (e.g. 'title'). + size (int or str): The font size for the style, either as an integer + (assumed to be in pt) or as a string specifying the unit of size + (e.g. '12pt' or '22px'). + color (~sdl2.ext.Color): The font color for the style. + bg_color (~sdl2.ext.Color, optional): The background surface color + for the style. Defaults to a fully-transparent background if not + specified. + + """ + # Preprocess and validate style values + self._check_if_closed() + if name in self._styles.keys(): + e = "A style named '{0}' is already defined for the current font." + raise ValueError(e.format(name)) + size_pt = self._parse_size(size) + if size_pt < 1: + e = "Font size must be at least 1pt, got {0} (specified as '{1}')" + raise ValueError(e.format(size_pt, size)) + c = convert_to_color(color) + if bg_color is not None: + bg_color = convert_to_color(bg_color) + + # Actually create font object for style + font = sdlttf.TTF_OpenFontIndexRW(self._font_rw, 0, size_pt, self._index) + if not font: + raise_sdl_err("initializing the '{0}' font style".format(name)) + + self._styles[name] = { + 'font': font, + 'color': pixels.SDL_Color(c.r, c.g, c.b, c.a), + 'bg': bg_color + } + + def render_text(self, text, style='default', line_h=None, width=None, align='left'): + """Renders a string of text to a new surface. + + If a newline character (``\\n``) is encountered in the string, it will + be rendered as a line break in the rendered text. Additionally, if a + surface width is specified, any lines of text that exceed the width of + the surface will be wrapped. Multi-line text can be left-aligned + (the default), right-aligned, or centered, and the spacing between lines + can be modified using the `line_h` argument. + + Args: + text (str): The string of text to render to the target surface. + style (str, optional): The font style with which to render the + given string. Defaults to the 'default' style if not specified. + line_h (int, optional): The line height (in pixels) to use for each + line of the rendered text. If not specified, the suggested line + height for the font will be used. Defaults to ``None``. + width (int, optional): The width (in pixels) of the output surface. + If a line of text exceeds this value, it will be automatically + wrapped to fit within the specified width. Defaults to ``None``. + align (str, optional): The alignment of lines of text for multi-line + strings. Can be 'left' (left-aligned), 'right' (right-aligned), + or 'center' (centered). Defaults to 'left'. + + Returns: + :obj:`~sdl2.SDL_Surface`: A 32-bit ARGB surface containing the + rendered text. + + """ + self._check_if_closed() + if len(text) == 0: + raise ValueError("Cannot render an empty string.") + if style not in self._styles.keys(): + e = "The '{0}' style is not defined for the current font." + raise ValueError(e.format(style)) + if line_h != None and (line_h != int(line_h) or line_h < 1): + e = "Line height must be a positive integer (got {0})." + raise ValueError(e.format(line_h)) + if align not in ["left", "right", "center"]: + e = "Text alignment mode must be 'left', 'right', or 'center'." + raise ValueError(e) + + # Actually render the text + lines = self._split_lines(text, style, width) + sf = self._render_lines(lines, style, line_h, width, align) + return sf.contents + + def close(self): + """Cleanly closes the font and frees all associated memory. + + This method should be called when you are finished with the font to free + the resources taken up by the font and its styles. Once this method has + been called, the font can no longer be used. + + """ + if self._font_rw != None: + for name, style in self._styles.items(): + sdlttf.TTF_CloseFont(style['font']) + self._styles = None + self._font_rw = None + + def contains(self, c): + """Checks whether a given character exists within the font. + + Args: + c (str): The glpyh (i.e. character) to check for within the font. + + Returns: + bool: ``True`` if the font contains the glpyh, otherwise ``False``. + + """ + self._check_if_closed() + font = self._styles['default']['font'] + return sdlttf.TTF_GlyphIsProvided(font, ord(c)) != 0 + + @property + def family_name(self): + """str: The family name (e.g. "Helvetica") of the loaded font. + + """ + self._check_if_closed() + name = sdlttf.TTF_FontFaceFamilyName(self._styles['default']['font']) + return stringify(name) if name else None + + @property + def style_name(self): + """str: The style name (e.g. "Bold") of the loaded font. + + """ + self._check_if_closed() + name = sdlttf.TTF_FontFaceStyleName(self._styles['default']['font']) + return stringify(name) if name else None + + @property + def is_fixed_width(self): + """bool: Whether the current font face is fixed-width (i.e. monospaced). + + If ``True``, all characters in the current font have the same width. + + """ + self._check_if_closed() + font = self._styles['default']['font'] + return sdlttf.TTF_FontFaceIsFixedWidth(font) != 0 + + + +class FontManager(object): + """A class for managing and rendering TrueType fonts. + + .. note:: + This class is has been deprecated in favor of the more flexible + :class:`~sdl2.ext.FontTTF` class. + + This class provides a basic wrapper around the SDL_ttf library. One font + path must be given to initialise the FontManager. + + The first face is always at index 0. It can be used for TTC (TrueType Font + Collection) fonts. + + Args: + font_path (str): The relative (or absolute) path to the font + to load. + alias (str, optional): The name to give the font within the + FontManager. Defaults to the font filename if not specified. + size (int, optional): The size (in pt) at which to load the default + font. Defaults to 16pt if not specified. + color (~sdl2.ext.Color): The default font rendering color. Defaults + to opaque white if not specified. + bg_color (~sdl2.ext.Color, optional): The default background surface + color. Defaults to a fully-transparent background if not specified. + index (int, optional): The index of the font face to load if the + font file contains multiple faces. Defaults to 0 (first face in + the file) if not specified. + + Attributes: + size (int): The default font size in pt. + + """ + def __init__(self, font_path, alias=None, size=16, + color=Color(255, 255, 255), bg_color=Color(0, 0, 0), index=0): + _ttf_init() + self.fonts = {} # fonts = {alias: {size:font_ptr}} + self.aliases = {} # aliases = {alias:font_path} + self._textcolor = None + self._bgcolor = None + self.color = color + self.bg_color = bg_color + self.size = size + self._default_font = self.add(font_path, alias, size, index) + + def __del__(self): + """Close all opened fonts.""" + self.close() + + def close(self): + """Closes all fonts opened by the class.""" + for alias, fonts in self.fonts.items(): + for size, font in fonts.items(): + if font: + sdlttf.TTF_CloseFont(font) + self.fonts = {} + self.aliases = {} + + def add(self, font_path, alias=None, size=None, index=0): + """Adds a font to the :class:`FontManager`. + + Args: + font_path (str): The relative (or absolute) path to the font + to load. + alias (str, optional): The name to give the font within the + FontManager. Defaults to the font filename if not specified. + size (int, optional): The size (in pt) at which to load the font. + Defaults to the FontManager's default size if not specified. + index (int, optional): The index of the font face to load if the + font file contains multiple faces. Defaults to 0 (first face in + the file) if not specified. + + Returns: + :obj:`~sdl2.sdlttf.TTF_Font`: A pointer to the ctypes font object + for the added font. + + """ + size = size or self.size + if alias is None: + # If no alias given, take the font name as alias + basename = os.path.basename(font_path) + alias = os.path.splitext(basename)[0] + if alias in self.fonts: + if size in self.fonts[alias] and self.fonts[alias]: + # font with selected size already opened + return + else: + self._change_font_size(alias, size) + return + else: + if not os.path.isfile(font_path): + raise IOError("Cannot find %s" % font_path) + + font = self._load_font(font_path, size, index) + self.aliases[alias] = font_path + self.fonts[alias] = {} + self.fonts[alias][size] = font + return font + + def _load_font(self, font_path, size, index=0): + """Helper function to open the font. + + Raises an exception if something went wrong. + """ + fullpath, fname = _validate_path(font_path, "a font") + fullpath = byteify(fullpath) + font = sdlttf.TTF_OpenFontIndex(fullpath, size, index) + if not font: + raise_sdl_err("opening the font '{0}'".format(fname)) + return font + + def _change_font_size(self, alias, size): + """Loads an already opened font in another size.""" + if alias not in self.fonts: + raise KeyError("Font %s not loaded in FontManager" % alias) + font = self._load_font(self.aliases[alias], size) + self.fonts[alias][size] = font + + @property + def color(self): + """:obj:`~sdl2.ext.Color`: The color to use for rendering text.""" + c = self._textcolor + return Color(c.r, c.g, c.b, c.a) + + @color.setter + def color(self, value): + c = convert_to_color(value) + self._textcolor = pixels.SDL_Color(c.r, c.g, c.b, c.a) + + @property + def bg_color(self): + """:obj:`~sdl2.ext.Color`: The background color to use for rendering.""" + c = self._bgcolor + return Color(c.r, c.g, c.b, c.a) + + @bg_color.setter + def bg_color(self, value): + c = convert_to_color(value) + self._bgcolor = pixels.SDL_Color(c.r, c.g, c.b, c.a) + + @property + def default_font(self): + """str: The name of the default font. Must be set to the alias of a + currently-loaded font. + + """ + for alias in self.fonts: + for size, font in self.fonts[alias].items(): + if font == self._default_font: + return alias + + @default_font.setter + def default_font(self, value): + alias = value + size = self.size + if alias not in self.fonts: + raise ValueError("Font %s not loaded in FontManager" % alias) + # Check if size is already loaded, otherwise do it. + if size not in self.fonts[alias]: + self._change_font_size(alias, size) + size = list(self.fonts[alias].keys())[0] + self._default_font = self.fonts[alias][size] + + def render(self, text, alias=None, size=None, width=None, color=None, + bg_color=None, **kwargs): + """Renders text to a surface. + + Args: + text (str): The text to render. + alias (str, optional): The alias of the font to use for rendering + the text. Defaults to the FontManager's default font if not + specified. + size (int, optional): The size (in pt) at which to render the font. + Defaults to the FontManager's default size if not specified. + width (int, optional): The width (in pixels) of the output surface. + If a line of text exceeds this value, it will be automatically + wrapped to fit within the specified width. Defaults to ``None``. + color (~sdl2.ext.Color): The font rendering color. Defaults to the + FontManager's default color if not specified. + bg_color (~sdl2.ext.Color, optional): The background surface color. + Defaults to the FontManager's default background color if not + specified. + + Returns: + :obj:`~sdl2.SDL_Surface`: A 32-bit ARGB surface containing the + rendered text. + + """ + alias = alias or self.default_font + size = size or self.size + if bg_color is None: + bg_color = self._bgcolor + elif not isinstance(bg_color, pixels.SDL_Color): + c = convert_to_color(bg_color) + bg_color = pixels.SDL_Color(c.r, c.g, c.b, c.a) + if color is None: + color = self._textcolor + elif not isinstance(color, pixels.SDL_Color): + c = convert_to_color(color) + color = pixels.SDL_Color(c.r, c.g, c.b, c.a) + if len(self.fonts) == 0: + raise TypeError("There are no fonts selected.") + font = self._default_font + if alias not in self.aliases: + raise KeyError("Font %s not loaded" % font) + elif size not in self.fonts[alias]: + self._change_font_size(alias, size) + font = self.fonts[alias][size] + text = byteify(text, "utf-8") + if width: + fontsf = sdlttf.TTF_RenderUTF8_Blended_Wrapped(font, text, color, + width) + if not fontsf: + raise SDLError(sdlttf.TTF_GetError()) + if bg_color != pixels.SDL_Color(0, 0, 0): + fontsf = fontsf.contents + w, h = fontsf.w, fontsf.h + bpp = fontsf.format.contents.BitsPerPixel + fmt = fontsf.format.contents.format + bgsf = surface.SDL_CreateRGBSurfaceWithFormat(0, w, h, bpp, fmt) + if not bgsf: + surface.SDL_FreeSurface(fontsf) + raise SDLError() + bg_color = prepare_color(bg_color, bgsf.contents) + surface.SDL_FillRect(bgsf, None, bg_color) + surface.SDL_BlitSurface(fontsf, None, bgsf, None) + return bgsf.contents + return fontsf.contents + sf = None + if bg_color == pixels.SDL_Color(0, 0, 0): + sf = sdlttf.TTF_RenderUTF8_Blended(font, text, color) + else: + sf = sdlttf.TTF_RenderUTF8_Shaded(font, text, color, + bg_color) + if not sf: + raise SDLError(sdlttf.TTF_GetError()) + return sf.contents diff --git a/sdl2/ext/gui.py b/sdl2/ext/uisystem.py similarity index 58% rename from sdl2/ext/gui.py rename to sdl2/ext/uisystem.py index 84b8d049..3542456d 100644 --- a/sdl2/ext/gui.py +++ b/sdl2/ext/uisystem.py @@ -1,19 +1,12 @@ """User interface elements.""" -from ctypes import byref, c_int, POINTER -from .color import Color -from .compat import isiterable, stringify, utf8 -from .common import SDLError +from .compat import isiterable, stringify from .ebs import System, World from .events import EventHandler from .sprite import Sprite -from .window import Window -from .. import (events, dll, mouse, keyboard, rect, error, SDL_PumpEvents, - SDL_Window) -from .. import messagebox as mb +from .. import events, mouse, keyboard, rect __all__ = [ "RELEASED", "HOVERED", "PRESSED", "BUTTON", "CHECKBUTTON", "TEXTENTRY", - "MessageBoxTheme", "MessageBox", "show_messagebox", "show_alert", "UIProcessor", "UIFactory" ] @@ -28,232 +21,6 @@ TEXTENTRY = 0x0004 -class MessageBoxTheme(object): - """Initializes a color scheme for use with :obj:`MessageBox` objects. - - This is used to define the background, text, and various button colors - to use when presenting dialog boxes to users. All colors must be defined - as either :obj:`sdl2.ext.Color` objects or 8-bit ``(r, g, b)`` tuples. - - .. note: SDL2 only supports MessageBox themes on a few platforms, including - Linux/BSD (if using X11) and Haiku. MessageBox themes will have no effect - on Windows, macOS, or Linux if using Wayland. - - Args: - bg (:obj:~`sdl2.ext.Color`, tuple, optional): The color to use for the - background of the dialog box. Defaults to ``(56, 54, 53)``. - text (:obj:~`sdl2.ext.Color`, tuple, optional): The color to use for the - text of the dialog box. Defaults to ``(209, 207, 205)``. - btn (:obj:~`sdl2.ext.Color`, tuple, optional): The color to use for the - backgrounds of buttons. Defaults to ``(140, 135, 129)``. - btn_border (:obj:~`sdl2.ext.Color`, tuple, optional): The color to use - for the borders of buttons. Defaults to ``(105, 102, 99)``. - btn_selected (:obj:~`sdl2.ext.Color`, tuple, optional): The color to use - for selected buttons. Defaults to ``(205, 202, 53)``. - - """ - def __init__( - self, bg=None, text=None, btn=None, btn_border=None, btn_selected=None - ): - # NOTE: Default colors taken from SDL_x11messagebox.c - self._theme = [ - (56, 54, 53), # Background color - (209, 207, 205), # Text color - (140, 135, 129), # Button border color - (105, 102, 99), # Button background color - (205, 202, 53) # Selected button color - ] - # Update default theme colors based on provided values - elements = [bg, text, btn_border, btn, btn_selected] - for i in range(len(elements)): - if elements[i] is not None: - self._theme[i] = self._validate_color(elements[i]) - - def _validate_color(self, col): - if not isinstance(col, Color): - if not isiterable(col) or len(col) != 3: - e = "MessageBox colors must be specified as (r, g, b) tuples." - raise TypeError(e) - for val in col: - if int(val) != float(val): - e = "All RGB values must be integers between 0 and 255." - raise ValueError(e) - col = Color(col[0], col[1], col[2]) - return (col.r, col.g, col.b) - - def _get_theme(self): - sdl_colors = [] - for col in self._theme: - sdl_colors.append(mb.SDL_MessageBoxColor(*col)) - col_array = (mb.SDL_MessageBoxColor * 5)(*sdl_colors) - return mb.SDL_MessageBoxColorScheme(col_array) - - -class MessageBox(object): - """Creates a prototype for a dialog box that can be presented to the user. - - The `MessageBox` class is for designing a dialog box in the style of the - system's window manager, containing a title, a message to present, and - one or more response buttons. - - Args: - title (str): The title to use for the dialog box. All UTF-8 characters - are supported. - msg (str): The main body of text to display in the dialog box. All UTF-8 - characters are supported. - buttons (list): A list of strings, containing the labels of the buttons - to place at the bottom of the dialog box (e.g. ``["No", "Yes"]``). - Buttons will be placed in left-to-right order. - default (str, optional): The label of the button to highlight as the - default option (e.g. ``"Yes"``). Must match one of the labels in - ``buttons``. This option will be accepted if the Return/Enter key - is pressed on the keyboard. - msgtype (str, optional): The type of dialog box to create, if supported - by the system. On most window managers, this changes the icon used - in the dialog box. Must be one of 'error', 'warning', or 'info', or - None (the default). - theme (:obj:`MessageBoxTheme`, optional): The color scheme to use for - the dialog box, if supported by the window manager. Defaults to the - system default theme. - - """ - def __init__(self, title, msg, buttons, default=None, msgtype=None, theme=None): - self._title = utf8(title).encode('utf-8') - self._text = utf8(msg).encode('utf-8') - self._validate_buttons(buttons) - self._buttons = buttons - self._sdlbuttons = self._init_buttons(buttons, default) - self._type = self._set_msgtype(msgtype) if msgtype else 0 - self._theme = theme._get_theme() if theme else None - - def _set_msgtype(self, msgtype): - _flagmap = { - 'error': mb.SDL_MESSAGEBOX_ERROR, - 'warning': mb.SDL_MESSAGEBOX_WARNING, - 'info': mb.SDL_MESSAGEBOX_INFORMATION, - } - if msgtype.lower() not in _flagmap.keys(): - raise ValueError( - "MessageBox type must be 'error', 'warning', 'info', or None." - ) - return _flagmap[msgtype] - - def _validate_buttons(self, buttons): - if not isiterable(buttons): - raise TypeError("Buttons must be provided as a list.") - elif len(buttons) == 0: - raise ValueError("MessageBox must have at least one button.") - - def _init_buttons(self, buttons, default): - default_flag = mb.SDL_MESSAGEBOX_BUTTON_RETURNKEY_DEFAULT - buttonset = [] - for i in range(len(buttons)): - b = mb.SDL_MessageBoxButtonData( - flags = (default_flag if buttons[i] == default else 0), - buttonid = i, - text = utf8(buttons[i]).encode('utf-8'), - ) - buttonset.append(b) - return (mb.SDL_MessageBoxButtonData * len(buttons))(*buttonset) - - def _get_window_pointer(self, win): - if isinstance(win, Window): - win = win.window - if isinstance(win, SDL_Window): - win = dll.get_pointer(win) - if hasattr(win, "contents") and isinstance(win.contents, SDL_Window): - return win - else: - e = "'window' must be a Window or SDL_Window object (got {0})" - raise ValueError(e.format(str(type(win)))) - - def _get_msgbox(self, window=None): - if window: - window = self._get_window_pointer(window) - return mb.SDL_MessageBoxData( - flags = self._type | mb.SDL_MESSAGEBOX_BUTTONS_RIGHT_TO_LEFT, - window = window, - title = self._title, - message = self._text, - numbuttons = len(self._buttons), - buttons = self._sdlbuttons, - colorScheme = dll.get_pointer(self._theme) if self._theme else None, - ) - - -def show_messagebox(msgbox, window=None): - """Displays a dialog box to the user and waits for a response. - - By default message boxes are presented independently of any window, but - they can optionally be attached explicitly to a specific SDL window. This - prevents that window from regaining focus until a response to the dialog - box is made. - - Args: - msgbox (:obj:`~sdl2.ext.MessageBox`): The dialog box to display - on-screen. - window (:obj:`~sdl2.SDL_Window`, :obj:`~sdl2.ext.Window`, optional): The - window to associate with the dialog box. Defaults to None. - - Returns: - str: The label of the button selected by the user. - - """ - resp = c_int(-1) - ret = mb.SDL_ShowMessageBox( - msgbox._get_msgbox(window), - byref(resp) - ) - SDL_PumpEvents() - if ret == 0: - return msgbox._buttons[resp.value] - else: - errmsg = error.SDL_GetError().decode('utf-8') - error.SDL_ClearError() - e = "Error encountered displaying message box" - if len(errmsg): - e += ": {0}".format(errmsg) - raise SDLError(e) - - -def show_alert(title, msg, msgtype=None, window=None): - """Displays a simple alert to the user and waits for a response. - - This function is a simplified version of :func:`show_messagebox` for cases - where only one response button ("OK") is needed and a custom color scheme - is not necessary. - - By default message boxes are presented independently of any window, but - they can optionally be attached explicitly to a specific SDL window. This - prevents that window from regaining focus until a response to the dialog - box is made. - - Args: - msgbox (:obj:`~sdl2.ext.MessageBox`): The dialog box to display - on-screen. - window (:obj:`~sdl2.SDL_Window`, :obj:`~sdl2.ext.Window`, optional): The - window to associate with the dialog box. Defaults to ``None``. - - """ - box = MessageBox(title, msg, ["OK"], msgtype=msgtype) - if window: - window = box._get_window_pointer(window) - ret = mb.SDL_ShowSimpleMessageBox( - box._type, - box._title, - box._text, - window - ) - SDL_PumpEvents() - if ret != 0: - errmsg = error.SDL_GetError().decode('utf-8') - error.SDL_ClearError() - e = "Error encountered displaying message box" - if len(errmsg): - e += ": {0}".format(errmsg) - raise SDLError(e) - - def _compose_button(obj): """Binds button attributes to the object, so it can be properly processed by the UIProcessor. diff --git a/sdl2/ext/window.py b/sdl2/ext/window.py index 4a5ac767..213a347b 100644 --- a/sdl2/ext/window.py +++ b/sdl2/ext/window.py @@ -22,25 +22,25 @@ class Window(object): ================================== ========================================= Flag Description ================================== ========================================= - sdl2.SDL_WINDOW_SHOWN Will be shown when created - sdl2.SDL_WINDOW_HIDDEN Will be hidden when created - sdl2.SDL_WINDOW_BORDERLESS Will not be decorated by the OS - sdl2.SDL_WINDOW_RESIZABLE Will be resizable - sdl2.SDL_WINDOW_MINIMIZED Will be created in a minimized state - sdl2.SDL_WINDOW_MAXIMIZED Will be created in a maximized state - sdl2.SDL_WINDOW_FULLSCREEN Will be fullscreen - sdl2.SDL_WINDOW_FULLSCREEN_DESKTOP Will be fullscreen at the current desktop + ``SDL_WINDOW_SHOWN`` Will be shown when created + ``SDL_WINDOW_HIDDEN`` Will be hidden when created + ``SDL_WINDOW_BORDERLESS`` Will not be decorated by the OS + ``SDL_WINDOW_RESIZABLE`` Will be resizable + ``SDL_WINDOW_MINIMIZED`` Will be created in a minimized state + ``SDL_WINDOW_MAXIMIZED`` Will be created in a maximized state + ``SDL_WINDOW_FULLSCREEN`` Will be fullscreen + ``SDL_WINDOW_FULLSCREEN_DESKTOP`` Will be fullscreen at the current desktop resolution - sdl2.SDL_WINDOW_OPENGL Will be usable with an OpenGL context - sdl2.SDL_WINDOW_VULKAN Will be usable with a Vulkan instance - sdl2.SDL_WINDOW_METAL Will be usable with a Metal context - sdl2.SDL_WINDOW_ALLOW_HIGHDPI Will be created in high-DPI mode + ``SDL_WINDOW_OPENGL`` Will be usable with an OpenGL context + ``SDL_WINDOW_VULKAN`` Will be usable with a Vulkan instance + ``SDL_WINDOW_METAL`` Will be usable with a Metal context + ``SDL_WINDOW_ALLOW_HIGHDPI`` Will be created in high-DPI mode (if supported) - sdl2.SDL_WINDOW_INPUT_FOCUS Will have input focus when created - sdl2.SDL_WINDOW_MOUSE_FOCUS Will have mouse focus when created - sdl2.SDL_WINDOW_INPUT_GRABBED Will prevent the mouse from leaving the + ``SDL_WINDOW_INPUT_FOCUS`` Will have input focus when created + ``SDL_WINDOW_MOUSE_FOCUS`` Will have mouse focus when created + ``SDL_WINDOW_INPUT_GRABBED`` Will prevent the mouse from leaving the bounds of the window - sdl2.SDL_WINDOW_MOUSE_CAPTURE Will capture mouse to track input outside + ``SDL_WINDOW_MOUSE_CAPTURE`` Will capture mouse to track input outside of the window when created ================================== ========================================= @@ -50,11 +50,11 @@ class Window(object): ================================== ===================================== Flag Description ================================== ===================================== - sdl2.SDL_WINDOW_ALWAYS_ON_TOP Should stay on top of other windows - sdl2.SDL_WINDOW_SKIP_TASKBAR Should not be added to the taskbar - sdl2.SDL_WINDOW_UTILITY Should be treated as a utility window - sdl2.SDL_WINDOW_TOOLTIP Should be treated as a tooltip - sdl2.SDL_WINDOW_POPUP_MENU Should be treated as a popup menu + ``SDL_WINDOW_ALWAYS_ON_TOP`` Should stay on top of other windows + ``SDL_WINDOW_SKIP_TASKBAR`` Should not be added to the taskbar + ``SDL_WINDOW_UTILITY`` Should be treated as a utility window + ``SDL_WINDOW_TOOLTIP`` Should be treated as a tooltip + ``SDL_WINDOW_POPUP_MENU`` Should be treated as a popup menu ================================== ===================================== To combine multiple flags, you can use a bitwise OR to combine two or more @@ -76,7 +76,7 @@ class Window(object): top-left corner of the window. If not specified, defaults to letting the system's window manager choose a location for the window. flags (int, optional): The window attribute flags with which the window - will be created. Defaults to `SDL_WINDOW_HIDDEN`. + will be created. Defaults to ``SDL_WINDOW_HIDDEN``. Attributes: window (:obj:`~sdl2.SDL_Window`, None): The underlying SDL2 Window @@ -215,8 +215,10 @@ def restore(self): def refresh(self): """Updates the window to reflect any changes made to its surface. - .. note: This only needs to be called if the window surface was - acquired and modified using :meth:`get_surface`. + .. note:: + This only needs to be called if the window surface was acquired and + modified using :meth:`get_surface`. + """ video.SDL_UpdateWindowSurface(self.window) @@ -232,9 +234,10 @@ def get_surface(self): such, you will need to call this method again whenever this happens in order to continue to access the window's contents. - .. note: If using OpenGL/Vulkan/Metal rendering or the SDL rendering - API (e.g. :func:`sdl2.SDL_CreateRenderer`) for drawing to the - window, this method should not be used. + .. note:: + If using OpenGL/Vulkan/Metal rendering or the SDL rendering API (e.g. + :func:`sdl2.SDL_CreateRenderer`) for drawing to the window, this + method should not be used. Returns: :obj:`~sdl2.SDL_Surface`: The surface associated with the window. diff --git a/sdl2/gamecontroller.py b/sdl2/gamecontroller.py index 24dfaedd..2e41e5e8 100644 --- a/sdl2/gamecontroller.py +++ b/sdl2/gamecontroller.py @@ -201,7 +201,7 @@ class SDL_GameControllerButtonBind(Structure): SDL_GameControllerGetSensorData = _bind("SDL_GameControllerGetSensorData", [POINTER(SDL_GameController), SDL_SensorType, POINTER(c_float), c_int], c_int, added='2.0.14') SDL_GameControllerAddMappingsFromRW = _bind("SDL_GameControllerAddMappingsFromRW", [POINTER(SDL_RWops), c_int], c_int) SDL_GameControllerAddMappingsFromFile = lambda fname: SDL_GameControllerAddMappingsFromRW(SDL_RWFromFile(fname, b"rb"), 1) -SDL_GameControllerFromInstanceID = _bind("SDL_GameControllerFromInstanceID", [SDL_JoystickID], POINTER(SDL_GameController)) +SDL_GameControllerFromInstanceID = _bind("SDL_GameControllerFromInstanceID", [SDL_JoystickID], POINTER(SDL_GameController), added='2.0.4') SDL_GameControllerFromPlayerIndex = _bind("SDL_GameControllerFromPlayerIndex", [c_int], POINTER(SDL_GameController), added='2.0.12') SDL_GameControllerGetPlayerIndex = _bind("SDL_GameControllerGetPlayerIndex", [POINTER(SDL_GameController)], c_int, added='2.0.9') SDL_GameControllerSetPlayerIndex = _bind("SDL_GameControllerSetPlayerIndex", [POINTER(SDL_GameController), c_int], added='2.0.12') diff --git a/sdl2/hints.py b/sdl2/hints.py index 410e3606..543ecade 100644 --- a/sdl2/hints.py +++ b/sdl2/hints.py @@ -275,7 +275,7 @@ SDL_SetHintWithPriority = _bind("SDL_SetHintWithPriority", [c_char_p, c_char_p, SDL_HintPriority], SDL_bool) SDL_SetHint = _bind("SDL_SetHint", [c_char_p, c_char_p], SDL_bool) SDL_GetHint = _bind("SDL_GetHint", [c_char_p], c_char_p) -SDL_GetHintBoolean = _bind("SDL_GetHintBoolean", [c_char_p, SDL_bool], SDL_bool) +SDL_GetHintBoolean = _bind("SDL_GetHintBoolean", [c_char_p, SDL_bool], SDL_bool, added='2.0.5') SDL_ClearHints = _bind("SDL_ClearHints") SDL_HintCallback = CFUNCTYPE(None, c_void_p, c_char_p, c_char_p, c_char_p) SDL_AddHintCallback = _bind("SDL_AddHintCallback", [c_char_p, SDL_HintCallback, c_void_p]) diff --git a/sdl2/joystick.py b/sdl2/joystick.py index e10d27ff..b8a185d4 100644 --- a/sdl2/joystick.py +++ b/sdl2/joystick.py @@ -121,8 +121,8 @@ class SDL_JoystickGUID(Structure): SDL_JoystickGetBall = _bind("SDL_JoystickGetBall", [POINTER(SDL_Joystick), c_int, POINTER(c_int), POINTER(c_int)], c_int) SDL_JoystickGetButton = _bind("SDL_JoystickGetButton", [POINTER(SDL_Joystick), c_int], Uint8) SDL_JoystickClose = _bind("SDL_JoystickClose", [POINTER(SDL_Joystick)]) -SDL_JoystickCurrentPowerLevel = _bind("SDL_JoystickCurrentPowerLevel", [POINTER(SDL_Joystick)], SDL_JoystickPowerLevel) -SDL_JoystickFromInstanceID = _bind("SDL_JoystickFromInstanceID", [SDL_JoystickID], POINTER(SDL_Joystick)) +SDL_JoystickCurrentPowerLevel = _bind("SDL_JoystickCurrentPowerLevel", [POINTER(SDL_Joystick)], SDL_JoystickPowerLevel, added='2.0.4') +SDL_JoystickFromInstanceID = _bind("SDL_JoystickFromInstanceID", [SDL_JoystickID], POINTER(SDL_Joystick), added='2.0.4') SDL_JoystickFromPlayerIndex = _bind("SDL_JoystickFromPlayerIndex", [c_int], POINTER(SDL_Joystick), added='2.0.12') SDL_JoystickAttachVirtual = _bind("SDL_JoystickAttachVirtual", [SDL_JoystickType, c_int, c_int, c_int], c_int, added='2.0.14') SDL_JoystickDetachVirtual = _bind("SDL_JoystickDetachVirtual", [c_int], c_int, added='2.0.14') diff --git a/sdl2/mouse.py b/sdl2/mouse.py index 734d22e9..873480c4 100644 --- a/sdl2/mouse.py +++ b/sdl2/mouse.py @@ -71,9 +71,9 @@ class SDL_Cursor(c_void_p): SDL_GetDefaultCursor = _bind("SDL_GetDefaultCursor", None, POINTER(SDL_Cursor)) SDL_FreeCursor = _bind("SDL_FreeCursor", [POINTER(SDL_Cursor)]) SDL_ShowCursor = _bind("SDL_ShowCursor", [c_int], c_int) -SDL_WarpMouseGlobal = _bind("SDL_WarpMouseGlobal", [c_int, c_int], c_int) -SDL_CaptureMouse = _bind("SDL_CaptureMouse", [SDL_bool], c_int) -SDL_GetGlobalMouseState = _bind("SDL_GetGlobalMouseState", [POINTER(c_int), POINTER(c_int)], Uint32) +SDL_WarpMouseGlobal = _bind("SDL_WarpMouseGlobal", [c_int, c_int], c_int, added='2.0.4') +SDL_CaptureMouse = _bind("SDL_CaptureMouse", [SDL_bool], c_int, added='2.0.4') +SDL_GetGlobalMouseState = _bind("SDL_GetGlobalMouseState", [POINTER(c_int), POINTER(c_int)], Uint32, added='2.0.4') SDL_BUTTON = lambda X: (1 << ((X) - 1)) SDL_BUTTON_LEFT = 1 SDL_BUTTON_MIDDLE = 2 diff --git a/sdl2/pixels.py b/sdl2/pixels.py index 55f19e59..f071e0a8 100644 --- a/sdl2/pixels.py +++ b/sdl2/pixels.py @@ -244,53 +244,53 @@ def SDL_ISPIXELFORMAT_ALPHA(pformat): SDL_PIXELFORMAT_NV21 = SDL_DEFINE_PIXELFOURCC('N', 'V', '2', '1') SDL_PIXELFORMAT_EXTERNAL_OES = SDL_DEFINE_PIXELFOURCC('O', 'E', 'S', ' ') -ALL_PIXELFORMATS = [ - SDL_PIXELFORMAT_INDEX1LSB, - SDL_PIXELFORMAT_INDEX1MSB, - SDL_PIXELFORMAT_INDEX4LSB, - SDL_PIXELFORMAT_INDEX4MSB, - SDL_PIXELFORMAT_INDEX8, - SDL_PIXELFORMAT_RGB332, - SDL_PIXELFORMAT_RGB444, - SDL_PIXELFORMAT_RGB555, - SDL_PIXELFORMAT_BGR555, - SDL_PIXELFORMAT_ARGB4444, - SDL_PIXELFORMAT_RGBA4444, - SDL_PIXELFORMAT_ABGR4444, - SDL_PIXELFORMAT_BGRA4444, - SDL_PIXELFORMAT_ARGB1555, - SDL_PIXELFORMAT_RGBA5551, - SDL_PIXELFORMAT_ABGR1555, - SDL_PIXELFORMAT_BGRA5551, - SDL_PIXELFORMAT_RGB565, - SDL_PIXELFORMAT_BGR565, - SDL_PIXELFORMAT_RGB24, - SDL_PIXELFORMAT_BGR24, - SDL_PIXELFORMAT_RGB888, - SDL_PIXELFORMAT_RGBX8888, - SDL_PIXELFORMAT_BGR888, - SDL_PIXELFORMAT_BGRX8888, - SDL_PIXELFORMAT_ARGB8888, - SDL_PIXELFORMAT_RGBA8888, - SDL_PIXELFORMAT_ABGR8888, - SDL_PIXELFORMAT_BGRA8888, - SDL_PIXELFORMAT_ARGB2101010, - SDL_PIXELFORMAT_RGBA32, - SDL_PIXELFORMAT_ARGB32, - SDL_PIXELFORMAT_BGRA32, - SDL_PIXELFORMAT_ABGR32, - SDL_PIXELFORMAT_YV12, - SDL_PIXELFORMAT_IYUV, - SDL_PIXELFORMAT_YUY2, - SDL_PIXELFORMAT_UYVY, - SDL_PIXELFORMAT_YVYU, - SDL_PIXELFORMAT_NV12, - SDL_PIXELFORMAT_NV21, - SDL_PIXELFORMAT_EXTERNAL_OES -] +NAME_MAP = { + 'INDEX1LSB': SDL_PIXELFORMAT_INDEX1LSB, + 'INDEX1MSB': SDL_PIXELFORMAT_INDEX1MSB, + 'INDEX4LSB': SDL_PIXELFORMAT_INDEX4LSB, + 'INDEX4MSB': SDL_PIXELFORMAT_INDEX4MSB, + 'INDEX8': SDL_PIXELFORMAT_INDEX8, + 'RGB332': SDL_PIXELFORMAT_RGB332, + 'RGB444': SDL_PIXELFORMAT_RGB444, + 'RGB555': SDL_PIXELFORMAT_RGB555, + 'BGR555': SDL_PIXELFORMAT_BGR555, + 'ARGB4444': SDL_PIXELFORMAT_ARGB4444, + 'RGBA4444': SDL_PIXELFORMAT_RGBA4444, + 'ABGR4444': SDL_PIXELFORMAT_ABGR4444, + 'BGRA4444': SDL_PIXELFORMAT_BGRA4444, + 'ARGB1555': SDL_PIXELFORMAT_ARGB1555, + 'RGBA5551': SDL_PIXELFORMAT_RGBA5551, + 'ABGR1555': SDL_PIXELFORMAT_ABGR1555, + 'BGRA5551': SDL_PIXELFORMAT_BGRA5551, + 'RGB565': SDL_PIXELFORMAT_RGB565, + 'BGR565': SDL_PIXELFORMAT_BGR565, + 'RGB24': SDL_PIXELFORMAT_RGB24, + 'BGR24': SDL_PIXELFORMAT_BGR24, + 'RGB888': SDL_PIXELFORMAT_RGB888, + 'RGBX8888': SDL_PIXELFORMAT_RGBX8888, + 'BGR888': SDL_PIXELFORMAT_BGR888, + 'BGRX8888': SDL_PIXELFORMAT_BGRX8888, + 'ARGB8888': SDL_PIXELFORMAT_ARGB8888, + 'RGBA8888': SDL_PIXELFORMAT_RGBA8888, + 'ABGR8888': SDL_PIXELFORMAT_ABGR8888, + 'BGRA8888': SDL_PIXELFORMAT_BGRA8888, + 'ARGB2101010': SDL_PIXELFORMAT_ARGB2101010, + 'RGBA32': SDL_PIXELFORMAT_RGBA32, + 'ARGB32': SDL_PIXELFORMAT_ARGB32, + 'BGRA32': SDL_PIXELFORMAT_BGRA32, + 'ABGR32': SDL_PIXELFORMAT_ABGR32, + 'YV12': SDL_PIXELFORMAT_YV12, + 'IYUV': SDL_PIXELFORMAT_IYUV, + 'YUY2': SDL_PIXELFORMAT_YUY2, + 'UYVY': SDL_PIXELFORMAT_UYVY, + 'YVYU': SDL_PIXELFORMAT_YVYU, + 'NV12': SDL_PIXELFORMAT_NV12, + 'NV21': SDL_PIXELFORMAT_NV21, + 'EXTERNAL_OES': SDL_PIXELFORMAT_EXTERNAL_OES, +} if sdl_version >= 2012: - ALL_PIXELFORMATS.append(SDL_PIXELFORMAT_BGR444) -ALL_PIXELFORMATS = tuple(ALL_PIXELFORMATS) + NAME_MAP['BGR444'] = SDL_PIXELFORMAT_BGR444 +ALL_PIXELFORMATS = tuple(NAME_MAP.values()) class SDL_Color(Structure): _fields_ = [("r", Uint8), diff --git a/sdl2/render.py b/sdl2/render.py index ed25d439..b42cc170 100644 --- a/sdl2/render.py +++ b/sdl2/render.py @@ -199,13 +199,13 @@ class SDL_Texture(c_void_p): SDL_RenderGetViewport = _bind("SDL_RenderGetViewport", [POINTER(SDL_Renderer), POINTER(SDL_Rect)]) SDL_RenderGetClipRect = _bind("SDL_RenderGetClipRect", [POINTER(SDL_Renderer), POINTER(SDL_Rect)]) SDL_RenderSetClipRect = _bind("SDL_RenderSetClipRect", [POINTER(SDL_Renderer), POINTER(SDL_Rect)], c_int) -SDL_RenderIsClipEnabled = _bind("SDL_RenderIsClipEnabled", [POINTER(SDL_Renderer)], SDL_bool) +SDL_RenderIsClipEnabled = _bind("SDL_RenderIsClipEnabled", [POINTER(SDL_Renderer)], SDL_bool, added='2.0.4') SDL_RenderSetScale = _bind("SDL_RenderSetScale", [POINTER(SDL_Renderer), c_float, c_float], c_int) SDL_RenderGetScale = _bind("SDL_RenderGetScale", [POINTER(SDL_Renderer), POINTER(c_float), POINTER(c_float)]) SDL_RenderWindowToLogical = _bind("SDL_RenderWindowToLogical", [POINTER(SDL_Renderer), c_int, c_int, POINTER(c_float), POINTER(c_float)], added='2.0.18') SDL_RenderLogicalToWindow = _bind("SDL_RenderLogicalToWindow", [POINTER(SDL_Renderer), c_float, c_float, POINTER(c_int), POINTER(c_int)], added='2.0.18') -SDL_RenderGetIntegerScale = _bind("SDL_RenderGetIntegerScale", [POINTER(SDL_Renderer)], SDL_bool) -SDL_RenderSetIntegerScale = _bind("SDL_RenderSetIntegerScale", [POINTER(SDL_Renderer), SDL_bool], c_int) +SDL_RenderGetIntegerScale = _bind("SDL_RenderGetIntegerScale", [POINTER(SDL_Renderer)], SDL_bool, added='2.0.5') +SDL_RenderSetIntegerScale = _bind("SDL_RenderSetIntegerScale", [POINTER(SDL_Renderer), SDL_bool], c_int, added='2.0.5') SDL_SetRenderDrawColor = _bind("SDL_SetRenderDrawColor", [POINTER(SDL_Renderer), Uint8, Uint8, Uint8, Uint8], c_int) SDL_GetRenderDrawColor = _bind("SDL_GetRenderDrawColor", [POINTER(SDL_Renderer), POINTER(Uint8), POINTER(Uint8), POINTER(Uint8), POINTER(Uint8)], c_int) SDL_SetRenderDrawBlendMode = _bind("SDL_SetRenderDrawBlendMode", [POINTER(SDL_Renderer), SDL_BlendMode], c_int) diff --git a/sdl2/test/render_test.py b/sdl2/test/render_test.py index 4c40b4f6..419362fb 100644 --- a/sdl2/test/render_test.py +++ b/sdl2/test/render_test.py @@ -10,6 +10,7 @@ from sdl2.rect import SDL_FPoint from sdl2.pixels import SDL_Color from sdl2 import render, video, surface, pixels, blendmode, rect +from sdl2.ext.compat import byteify, stringify from sdl2.ext.pixelaccess import PixelView # TODO: Ensure all functions in module have corresponding tests @@ -142,20 +143,30 @@ def test_SDL_GetNumRenderDrivers(self): def test_SDL_GetRenderDriverInfo(self): renderers = [] errs = [] + pxformats = {} drivers = render.SDL_GetNumRenderDrivers() for x in range(drivers): sdl2.SDL_ClearError() info = render.SDL_RendererInfo() ret = render.SDL_GetRenderDriverInfo(x, info) if ret != 0: - err = sdl2.SDL_GetError().decode("utf-8") + err = stringify(sdl2.SDL_GetError()) errs.append("Renderer {0} error: {1}".format(x, err)) - else: - renderers.append(info.name.decode("utf-8")) - print("Render drivers supported by current SDL2 binary:") - print(renderers) + continue + rname = stringify(info.name) + renderers.append(rname) + pxformats[rname] = [] + for i in range(info.num_texture_formats): + fmt_name = pixels.SDL_GetPixelFormatName(info.texture_formats[i]) + pxformats[rname].append(stringify(fmt_name).split("_")[-1]) assert len(renderers) assert "software" in renderers + print("Render drivers supported by current SDL2 binary:") + print(renderers) + print("\nTexture formats supported by each renderer:") + for rname in renderers: + print(rname) + print(" - " + " ".join(pxformats[rname])) def test_SDL_CreateWindowAndRenderer(self): window = POINTER(video.SDL_Window)() @@ -238,34 +249,44 @@ def test_SDL_GetRenderer(self): dogc() def test_SDL_GetRendererInfo(self): - failed = 0 + renderers = [] + max_sizes = {} + errs = [] rcount = render.SDL_GetNumRenderDrivers() for i in range(rcount): - window = video.SDL_CreateWindow(b"Test", 10, 10, 10, 10, - video.SDL_WINDOW_HIDDEN) + sdl2.SDL_ClearError() + window = video.SDL_CreateWindow( + b"Test", 10, 10, 10, 10, video.SDL_WINDOW_HIDDEN + ) assert isinstance(window.contents, video.SDL_Window) renderer = render.SDL_CreateRenderer(window, i, self._RENDERFLAGS) if not (renderer and renderer.contents): - failed += 1 + err = stringify(sdl2.SDL_GetError()) + errs.append("Unable to create renderer {0}: {1}".format(i, err)) video.SDL_DestroyWindow(window) continue assert isinstance(renderer.contents, render.SDL_Renderer) info = render.SDL_RendererInfo() ret = render.SDL_GetRendererInfo(renderer, byref(info)) - assert ret == 0 + if ret == 0: + rname = stringify(info.name) + max_size = (info.max_texture_width, info.max_texture_height) + renderers.append(rname) + max_sizes[rname] = max_size + else: + err = stringify(sdl2.SDL_GetError()) + errs.append("Renderer {0} error: {1}".format(i, err)) render.SDL_DestroyRenderer(renderer) - - #self.assertRaises(sdl.SDLError, render.SDL_GetRendererInfo, - # renderer) - video.SDL_DestroyWindow(window) - assert not (failed == rcount), "could not create a renderer" - with pytest.raises((AttributeError, TypeError)): - render.SDL_GetRendererInfo(None) - with pytest.raises((AttributeError, TypeError)): - render.SDL_GetRendererInfo("Test") dogc() + assert len(renderers) + assert "software" in renderers + print("Render drivers loadable on the current system:") + for rname in renderers: + w, h = max_sizes[rname] + print(" - " + rname + " (max texture size: {0}x{1})".format(w, h)) + def test_SDL_CreateDestroyTexture(self): window = video.SDL_CreateWindow(b"Test", 10, 10, 10, 10, video.SDL_WINDOW_HIDDEN) diff --git a/sdl2/test/sdl2ext_draw_test.py b/sdl2/test/sdl2ext_draw_test.py index ac11bc81..87e4cb0c 100644 --- a/sdl2/test/sdl2ext_draw_test.py +++ b/sdl2/test/sdl2ext_draw_test.py @@ -3,7 +3,6 @@ from sdl2.surface import SDL_CreateRGBSurface from sdl2.rect import SDL_Rect from sdl2.ext.color import Color, COLOR -from sdl2.ext.compat import ExperimentalWarning from sdl2 import ext as sdl2ext try: @@ -32,8 +31,7 @@ def test_fill(self): WHITE = (255, 255, 255) BLACK = (0, 0, 0) sf = SDL_CreateRGBSurface(0, 10, 10, 32, 0, 0, 0, 0) - with pytest.warns(ExperimentalWarning): - view = sdl2ext.pixels3d(sf.contents, False) + view = sdl2ext.pixels3d(sf.contents, False) # Test with no provided fill area sdl2ext.fill(sf.contents, WHITE, None) @@ -75,8 +73,7 @@ def test_line(self): WHITE = (255, 255, 255) BLACK = (0, 0, 0) sf = SDL_CreateRGBSurface(0, 10, 10, 32, 0, 0, 0, 0) - with pytest.warns(ExperimentalWarning): - view = sdl2ext.pixels3d(sf.contents, False) + view = sdl2ext.pixels3d(sf.contents, False) # Test with a single straight line sdl2ext.line(sf.contents, WHITE, (0, 0, 11, 0)) diff --git a/sdl2/test/sdl2ext_font_test.py b/sdl2/test/sdl2ext_font_test.py index afc0930a..905cf428 100644 --- a/sdl2/test/sdl2ext_font_test.py +++ b/sdl2/test/sdl2ext_font_test.py @@ -1,14 +1,21 @@ # -*- coding: utf-8 -*- import pytest +import sdl2 from sdl2 import ext as sdl2ext -from sdl2.ext.compat import byteify, ExperimentalWarning +from sdl2.ext.compat import byteify from sdl2.ext.pixelaccess import pixels2d -from sdl2 import surface +from sdl2.ext.surface import _create_surface +from sdl2 import surface, pixels, SDL_ClearError, SDL_GetError -sdlttf = pytest.importorskip("sdl2.sdlttf") +_HASSDLTTF = True +try: + from .. import sdlttf +except ImportError: + _HASSDLTTF = False RESOURCES = sdl2ext.Resources(__file__, "resources") +RED_RGBA = (255, 0, 0, 255) FONTMAP = ["0123456789", "ABCDEFGHIJ", "KLMNOPQRST", @@ -20,7 +27,25 @@ ] -class TestSDL2ExtFont(object): +@pytest.fixture(scope="module") +def with_sdl(): + sdl2ext.init() + yield + sdl2ext.quit() + +@pytest.fixture() +def with_font_ttf(with_sdl): + if _HASSDLTTF: + SDL_ClearError() + fontpath = RESOURCES.get_path("tuffy.ttf") + font = sdl2ext.FontTTF(fontpath, 20, RED_RGBA) + assert SDL_GetError() == b"" + assert font + yield font + font.close() + + +class TestBitmapFont(object): __tags__ = ["sdl", "sdl2ext"] @classmethod @@ -34,7 +59,7 @@ def setup_class(cls): def teardown_class(cls): sdl2ext.quit() - def test_BitmapFont(self): + def test_init(self): # Initialize surface and sprite for tests fontpath = byteify(RESOURCES.get_path("font.bmp"), "utf-8") sf = surface.SDL_LoadBMP(fontpath) @@ -52,11 +77,19 @@ def test_BitmapFont(self): font = sdl2ext.BitmapFont(sf, (32, 32), FONTMAP) assert font.size == (32, 32) + # Try loading a font directly from a .bmp + font = sdl2ext.BitmapFont(fontpath, (32, 32), FONTMAP) + assert font.size == (32, 32) + + # Test use of default fontmap and inferred character size + font = sdl2ext.BitmapFont(sf) + assert font.size == (32, 32) + # Try invalid surface type with pytest.raises(TypeError): - font = sdl2ext.BitmapFont("hello", (32, 32), FONTMAP) + sdl2ext.BitmapFont([], (32, 32), FONTMAP) - def test_BitmapFont_render(self): + def test_render(self): # Initialize font and BitmapFont for tests fontpath = byteify(RESOURCES.get_path("font.bmp"), "utf-8") sf = surface.SDL_LoadBMP(fontpath) @@ -72,46 +105,119 @@ def test_BitmapFont_render(self): with pytest.raises(ValueError): font.render("this_should_fail") - def test_BitmapFont_render_on(self): + def test_render_text(self): + # Initialize BitmapFont and dummy RGB888 surface for tests + fontpath = byteify(RESOURCES.get_path("font.bmp"), "utf-8") + font = sdl2ext.BitmapFont(fontpath) + rgb_surf = _create_surface(size=(320, 256), fmt="RGB888") + font_rgb = sdl2ext.BitmapFont(rgb_surf) + + # Try rendering some text + msg = "hello there!" + text = font.render_text(msg) + assert isinstance(text, surface.SDL_Surface) + assert text.w == 32 * len(msg) + assert text.h == 32 + surface.SDL_FreeSurface(text) + + # Try rendering with a different line height + text = font.render_text(msg, line_h=40) + assert isinstance(text, surface.SDL_Surface) + assert text.w == 32 * len(msg) + assert text.h == 40 + surface.SDL_FreeSurface(text) + + # Make sure ARGB converion works + text = font_rgb.render_text(msg) + assert isinstance(text, surface.SDL_Surface) + assert text.w == 32 * len(msg) + assert text.h == 32 + assert text.format.contents.format == pixels.SDL_PIXELFORMAT_ARGB8888 + surface.SDL_FreeSurface(text) + + # Try rendering without ARGB conversion + text = font_rgb.render_text(msg, as_argb=False) + assert isinstance(text, surface.SDL_Surface) + assert text.w == 32 * len(msg) + assert text.h == 32 + assert text.format.contents.format == pixels.SDL_PIXELFORMAT_RGB888 + surface.SDL_FreeSurface(text) + + # Test multiline rendering + msg = "hello\nthere!" + text = font.render_text(msg) + assert isinstance(text, surface.SDL_Surface) + assert text.w == 32 * 6 + assert text.h == 64 + surface.SDL_FreeSurface(text) + + # Test exception for missing glyph + with pytest.raises(ValueError): + font.render_text("this_should_fail") + + def test_render_on(self): np = pytest.importorskip("numpy", reason="numpy module is not available") - # Initialize font, surface, and BitmapFont for tests + # Initialize BitmapFont for tests fontpath = byteify(RESOURCES.get_path("font.bmp"), "utf-8") - sf = surface.SDL_LoadBMP(fontpath) - font = sdl2ext.BitmapFont(sf.contents, (32, 32), FONTMAP) + font = sdl2ext.BitmapFont(fontpath) # Try rendering some text - target = surface.SDL_CreateRGBSurface(0, 32*5, 32, 32, 0, 0, 0, 0) - with pytest.warns(ExperimentalWarning): - view = pixels2d(target, transpose=False) + target = _create_surface(size=(32*5, 32)) + view = pixels2d(target, transpose=False) mid_row = view[16, :].copy() - font.render_on(target, "TEST!") + outrect = font.render_on(target, "TEST!") assert not np.all(mid_row == view[16, :]) # ensure surface changed + assert outrect == (0, 0, 32*5, 32) # Try rendering some text with an offset - target2 = surface.SDL_CreateRGBSurface(0, 32*5, 32, 32, 0, 0, 0, 0) - with pytest.warns(ExperimentalWarning): - view2 = pixels2d(target2, transpose=False) - font.render_on(target2, "TEST!", offset=(5, 0)) + target2 = _create_surface(size=(32*5, 32)) + view2 = pixels2d(target2, transpose=False) + outrect2 = font.render_on(target2, "TEST!", offset=(5, 0)) assert not np.all(mid_row == view2[16, :]) # ensure surface changed assert not np.all(view[16, :] == view2[16, :]) # ensure offset worked + assert outrect2 == (5, 0, 32*5 + 5, 32) + + surface.SDL_FreeSurface(target) + surface.SDL_FreeSurface(target2) # Test exception for missing glyph with pytest.raises(ValueError): font.render_on(target, "%nope") - def test_BitmapFont_contains(self): - sf = surface.SDL_LoadBMP(byteify(RESOURCES.get_path("font.bmp"), - "utf-8")) - assert isinstance(sf.contents, surface.SDL_Surface) - font = sdl2ext.BitmapFont(sf, (32, 32), FONTMAP) - assert isinstance(font, sdl2ext.BitmapFont) + def test_remap(self): + # Initialize BitmapFont for tests + fontpath = byteify(RESOURCES.get_path("font.bmp"), "utf-8") + font = sdl2ext.BitmapFont(fontpath) + msg = "hello there!" + + # Remap the l to be narrower and try rendering + font.remap("l", 32, 160, 12, 32) + text = font.render_text(msg) + assert text.w == (32 * len(msg) - 40) + assert text.h == 32 + + surface.SDL_FreeSurface(text) + + # Test exceptions on bad input + with pytest.raises(ValueError): + font.remap("hi", 4, 4, 4, 4) + with pytest.raises(ValueError): + font.remap("h", 10, 32, 32, 0) + with pytest.raises(ValueError): + font.remap("h", 32, 1000, 32, 32) + + def test_contains(self): + # Initialize BitmapFont for tests + fontpath = byteify(RESOURCES.get_path("font.bmp"), "utf-8") + font = sdl2ext.BitmapFont(fontpath) for ch in "abcde12345.-,+": assert font.contains(ch) - for ch in "äöüß": + del font.offsets[" "] + for ch in u" äöüß": assert not font.contains(ch) - def test_BitmapFont_can_render(self): + def test_can_render(self): sf = surface.SDL_LoadBMP(byteify(RESOURCES.get_path("font.bmp"), "utf-8")) assert isinstance(sf.contents, surface.SDL_Surface) @@ -122,7 +228,220 @@ def test_BitmapFont_can_render(self): assert font.can_render("473285435hfsjadfhriuewtrhefd") assert not font.can_render("testä") - def test_FontManager(self): + + +@pytest.mark.skipif(not _HASSDLTTF, reason="SDL_TTF library not available") +class TestFontTTF(object): + __tags__ = ["sdl", "sdl2ext"] + + def test_init(self, with_sdl): + # Try opening and closing a font + fontpath = RESOURCES.get_path("tuffy.ttf") + font = sdl2ext.FontTTF(fontpath, 20, RED_RGBA) + assert SDL_GetError() == b"" + assert font + font.close() + + # Try opening a font with size specified in pt + font = sdl2ext.FontTTF(fontpath, "20pt", RED_RGBA) + assert SDL_GetError() == b"" + assert font + font.close() + + # Try opening a font with size specified in pixels + font = sdl2ext.FontTTF(fontpath, "20px", RED_RGBA) + assert SDL_GetError() == b"" + assert font + + # Try opening a font with a custom set of height chars + chars = "aeiou" + font2 = sdl2ext.FontTTF(fontpath, "20px", RED_RGBA, height_chars=chars) + assert SDL_GetError() == b"" + assert font + assert font._parse_size("20px") != font2._parse_size("20px") + font.close() + font2.close() + + # Test exception on invalid font path + with pytest.raises(IOError): + sdl2ext.FontTTF("missing.ttf", 16, RED_RGBA) + + # Text exception on invalid size + with pytest.raises(ValueError): + sdl2ext.FontTTF(fontpath, 0, RED_RGBA) + + # Test exception on missing height chars + with pytest.raises(RuntimeError): + sdl2ext.FontTTF(fontpath, 20, RED_RGBA, height_chars=u"的是不") + + def test_get_ttf_font(self, with_font_ttf): + font = with_font_ttf + ttf_font = font.get_ttf_font() + assert isinstance(ttf_font, sdlttf.TTF_Font) + + # Test exception for missing style + with pytest.raises(ValueError): + font.get_ttf_font("small") + + def test_add_style(self, with_font_ttf): + font = with_font_ttf + + # Add some new styles + font.add_style('large', '40px', RED_RGBA) + font.add_style('red', 20, pixels.SDL_Color(255, 0, 0)) + font.add_style('red_on_white', 20, (255, 0, 0), (255, 255, 255)) + + # Test exception for existing style name + with pytest.raises(ValueError): + font.add_style('large', '50px', RED_RGBA) + + # Test exception for invalid size unit + with pytest.raises(ValueError): + font.add_style('unit_err', '10cm', RED_RGBA) + + # Test exception for non-integer size + with pytest.raises(ValueError): + font.add_style('float_err', '10.5pt', RED_RGBA) + + def test_render_text(self, with_font_ttf): + font = with_font_ttf + + # Try rendering some text + msg = "hello there!" + text = font.render_text(msg) + assert SDL_GetError() == b"" + assert isinstance(text, surface.SDL_Surface) + assert text.format.contents.format == pixels.SDL_PIXELFORMAT_ARGB8888 + + # Test multiline rendering + msg = "hello\nthere!" + text2 = font.render_text(msg) + assert SDL_GetError() == b"" + assert isinstance(text2, surface.SDL_Surface) + assert text2.h > text.h + + # Test custom line height + msg = "hello\nthere!" + text3 = font.render_text(msg, line_h=100) + assert SDL_GetError() == b"" + assert isinstance(text3, surface.SDL_Surface) + assert text3.h > text2.h + surface.SDL_FreeSurface(text3) + + # Test wrap width + msg = "hello there! This is a very long line of text." + text3 = font.render_text(msg, width=200) + assert SDL_GetError() == b"" + assert isinstance(text3, surface.SDL_Surface) + assert text3.h > text.h + surface.SDL_FreeSurface(text3) + + # Test custom font style (no bg color) + msg = "hello there!" + font.add_style("blue", 30, (0, 0, 255, 255)) + text3 = font.render_text(msg, "blue") + assert SDL_GetError() == b"" + assert isinstance(text3, surface.SDL_Surface) + surface.SDL_FreeSurface(text3) + + # Test custom font style (w/ bg color) + msg = "hello there!" + font.add_style("red_on_white", 30, RED_RGBA, (255, 255, 255)) + text3 = font.render_text(msg, "red_on_white") + assert SDL_GetError() == b"" + assert isinstance(text3, surface.SDL_Surface) + surface.SDL_FreeSurface(text3) + + # Test font alignment (not extensive) + msg = "hello there!\nThis is a very long line of\ntext." + for align in ["left", "center", "right"]: + tmp = font.render_text(msg, align=align) + assert SDL_GetError() == b"" + assert isinstance(text3, surface.SDL_Surface) + surface.SDL_FreeSurface(tmp) + + surface.SDL_FreeSurface(text) + surface.SDL_FreeSurface(text2) + + # Test exception for empty string + with pytest.raises(ValueError): + font.render_text("") + + # Test exception for missing style + with pytest.raises(ValueError): + font.render_text(msg, "small") + + # Test exceptions for bad line height + with pytest.raises(ValueError): + font.render_text(msg, line_h=12.4) + with pytest.raises(ValueError): + font.render_text(msg, line_h=0) + + # Test exception for bad alignment + with pytest.raises(ValueError): + font.render_text(msg, align="flush") + + def test_close(self, with_font_ttf): + font = with_font_ttf + font.close() + + # Test that you can close the font multiple times + font.close() + + # Make sure you can't use font after closing it + with pytest.raises(RuntimeError): + font.get_ttf_font() + with pytest.raises(RuntimeError): + font.add_style('large', 40, RED_RGBA) + with pytest.raises(RuntimeError): + font.render_text("hello!") + with pytest.raises(RuntimeError): + font.contains("A") + with pytest.raises(RuntimeError): + font.family_name + with pytest.raises(RuntimeError): + font.style_name + with pytest.raises(RuntimeError): + font.is_fixed_width + + def test_contains(self, with_font_ttf): + font = with_font_ttf + for ch in "abcde12345": + assert font.contains(ch) + for ch in u"的是不": + assert not font.contains(ch) + + def test_family_name(self, with_font_ttf): + font = with_font_ttf + name = font.family_name + assert name == None or isinstance(name, str) + + def test_style_name(self, with_font_ttf): + font = with_font_ttf + name = font.style_name + assert name == None or isinstance(name, str) + + def test_is_fixed_width(self, with_font_ttf): + font = with_font_ttf + assert font.is_fixed_width == False + + +@pytest.mark.skipif(not _HASSDLTTF, reason="SDL_TTF library not available") +class TestFontManager(object): + __tags__ = ["sdl", "sdl2ext"] + + @classmethod + def setup_class(cls): + try: + sdl2ext.init() + except sdl2ext.SDLError: + raise pytest.skip('Video subsystem not supported') + + @classmethod + def teardown_class(cls): + sdl2ext.quit() + + def test_init(self): fm = sdl2ext.FontManager(RESOURCES.get_path("tuffy.ttf"), bg_color=(100, 0, 0)) assert isinstance(fm, sdl2ext.FontManager) @@ -130,7 +449,7 @@ def test_FontManager(self): assert fm.size == 16 assert fm.bg_color == sdl2ext.Color(100, 0, 0, 0) - def test_FontManager_default_font(self): + def test_default_font(self): fm = sdl2ext.FontManager(RESOURCES.get_path("tuffy.ttf")) assert fm.default_font == "tuffy" assert fm.size == 16 @@ -146,7 +465,7 @@ def test_FontManager_default_font(self): assert fm.default_font == "tuffy.copy" assert fm.size == 16 - def test_FontManager_add(self): + def test_add(self): fm = sdl2ext.FontManager(RESOURCES.get_path("tuffy.ttf")) assert "tuffy" in fm.aliases assert "tuffy" in fm.fonts @@ -173,7 +492,7 @@ def test_FontManager_add(self): fm.add(RESOURCES.get_path("tuffy.ttf"), size=12) assert isinstance(fm.fonts["tuffy"][12].contents, sdlttf.TTF_Font) - def test_FontManager_close(self): + def test_close(self): fm = sdl2ext.FontManager(RESOURCES.get_path("tuffy.ttf")) fm.add(RESOURCES.get_path("tuffy.ttf"), size=20) fm.add(RESOURCES.get_path("tuffy.ttf"), alias="Foo", size=10) @@ -181,7 +500,7 @@ def test_FontManager_close(self): assert fm.fonts == {} # How to make sure TTF_CloseFont was called on each loaded font? - def test_FontManager_render(self): + def test_render(self): fm = sdl2ext.FontManager(RESOURCES.get_path("tuffy.ttf")) text_surf = fm.render("text") assert isinstance(text_surf, surface.SDL_Surface) diff --git a/sdl2/test/sdl2ext_image_test.py b/sdl2/test/sdl2ext_image_test.py index 55ef6280..7ae7b38e 100644 --- a/sdl2/test/sdl2ext_image_test.py +++ b/sdl2/test/sdl2ext_image_test.py @@ -1,7 +1,11 @@ +import os import sys import pytest +import sdl2 from sdl2 import ext as sdl2ext +from sdl2.ext import color from sdl2 import surface +from sdl2 import pixels try: from sdl2 import sdlimage @@ -9,45 +13,50 @@ except: _HASSDLIMAGE=False -RESOURCES = sdl2ext.Resources(__file__, "resources") +try: + import PIL + _HASPIL = True +except ImportError: + _HASPIL = False + + +parent_path = os.path.abspath(os.path.dirname(__file__)) +resource_path = os.path.join(parent_path, "resources") is32bit = sys.maxsize <= 2**32 ismacos = sys.platform == "darwin" +skip_formats = [] + +if _HASSDLIMAGE: + # SVG unsupported on SDL2_image < 2.0.2 + if sdlimage.dll.version < 2002: + skip_formats.append("svg") + + # As of SDL2_image 2.0.5, XCF support seems to be broken (fails to load + # on 32-bit, transparent surface on 64-bit) + # XCF support is also broken in official SDL2_image macOS .frameworks + if sdlimage.dll.version == 2005 or ismacos: + skip_formats.append("xcf") + + # WEBP support is broken in the 32-bit Windows SDL2_image 2.0.2 binary + if is32bit and sdlimage.dll.version == 2002: + skip_formats.append("webp") + -formats = [ # Do not use bmp - it's contained in resources.zip - "cur", - "gif", - "ico", - "jpg", - "lbm", - "pbm", - "pcx", - "pgm", - "png", - "pnm", - "ppm", - "svg", - "tga", - "tif", - "webp", - "xcf", - "xpm", - # "xv", - ] - -# SVG unsupported on SDL2_image < 2.0.2 -if _HASSDLIMAGE and sdlimage.dll.version < 2002: - formats.remove("svg") - -# As of SDL2_image 2.0.5, XCF support seems to be broken on 32-bit builds -# XCF support is also broken in official SDL2_image macOS .frameworks -if is32bit or ismacos: - formats.remove("xcf") - -# WEBP support seems to be broken in the 32-bit Windows SDL2_image 2.0.2 binary -bad_webp = is32bit and sdlimage.dll.version == 2002 -if bad_webp: - formats.remove("webp") +# List of lossy/non-color formats that shouldn't be compared against reference +# during tests +skip_color_check = ['gif', 'jpg', 'lbm', 'pbm', 'pgm', 'svg', 'webp'] + +# SDL 2.0.10 has a bug that messes up converting surfaces with transparency +if sdl2.dll.version == 2010: + skip_color_check.append('xpm') + +colors = { + 'red': color.Color(255, 0, 0, 255), + 'blue': color.Color(0, 0, 255, 255), + 'black': color.Color(0, 0, 0, 255), + 'white': color.Color(255, 255, 255, 255) +} class TestSDL2ExtImage(object): @@ -64,33 +73,174 @@ def setup_class(cls): def teardown_class(cls): sdl2ext.quit() - def test_get_image_formats(self): - assert isinstance(sdl2ext.get_image_formats(), tuple) - supformats = sdl2ext.get_image_formats() - for fmt in formats: - assert fmt in supformats + + def check_image_contents(self, img): + # Test different coordinates on surface + pxview = sdl2ext.PixelView(img) + img_red = color.ARGB(pxview[0][0]) + img_blue = color.ARGB(pxview[0][16]) + img_white = color.ARGB(pxview[0][31]) + img_black = color.ARGB(pxview[31][31]) + assert img_red == colors['red'] + assert img_blue == colors['blue'] + assert img_white == colors['white'] + assert img_black == colors['black'] + + def test_load_bmp(self): + # Test loading a basic BMP image + img_path = os.path.join(resource_path, "surfacetest.bmp") + sf = sdl2ext.load_bmp(img_path) + assert isinstance(sf, surface.SDL_Surface) + self.check_image_contents(sf) + surface.SDL_FreeSurface(sf) + + # Test exception on missing file + bad_path = os.path.join(resource_path, "doesnt_exist.bmp") + with pytest.raises(IOError): + sdl2ext.load_bmp(bad_path) + + # Test exception on bad file type + bad_type = os.path.join(resource_path, "surfacetest.png") + with pytest.raises(sdl2ext.SDLError): + sdl2ext.load_bmp(bad_type) + + + def test_save_bmp(self, tmpdir): + # Open a BMP that we can re-save + img_path = os.path.join(resource_path, "surfacetest.bmp") + sf = sdl2ext.load_bmp(img_path) + assert isinstance(sf, surface.SDL_Surface) + + # Try saving the BMP to a new folder and re-loading it + outpath = os.path.join(str(tmpdir), "save_test.bmp") + sdl2ext.save_bmp(sf, outpath) + assert os.path.exists(outpath) + sf_saved = sdl2ext.load_bmp(outpath) + assert isinstance(sf_saved, surface.SDL_Surface) + self.check_image_contents(sf_saved) + + # Try modifying/overwriting the existing BMP + sdl2ext.fill(sf, (0, 255, 0, 255)) + sdl2ext.save_bmp(sf, outpath, overwrite=True) + sf_saved2 = sdl2ext.load_bmp(outpath) + assert isinstance(sf_saved2, surface.SDL_Surface) + with pytest.raises(AssertionError): + self.check_image_contents(sf_saved2) + + surface.SDL_FreeSurface(sf) + surface.SDL_FreeSurface(sf_saved) + surface.SDL_FreeSurface(sf_saved2) + + # Test existing file exception with overwrite=False + with pytest.raises(RuntimeError): + sdl2ext.save_bmp(sf_saved, outpath, overwrite=False) + + # Test exception with non-existent save directory + bad_path = os.path.join(resource_path, "doesnt_exist", "tst.bmp") + with pytest.raises(IOError): + sdl2ext.save_bmp(sf_saved, bad_path) + + + def test_load_img(self): + # Test loading all test images, with and without ARGB conversion + resources = os.listdir(resource_path) + test_imgs = [f for f in resources if f[:11] == "surfacetest"] + for img in test_imgs: + img_path = os.path.join(resource_path, img) + fmt = img.split(".")[-1] + if fmt in skip_formats: + continue + + sf = sdl2ext.load_img(img_path) + assert isinstance(sf, surface.SDL_Surface) + assert sf.format.contents.format == pixels.SDL_PIXELFORMAT_ARGB8888 + if fmt not in skip_color_check: + self.check_image_contents(sf) + surface.SDL_FreeSurface(sf) + + sf2 = sdl2ext.load_img(img_path, as_argb=False) + assert isinstance(sf2, surface.SDL_Surface) + surface.SDL_FreeSurface(sf2) + + # Test exception on missing file + bad_path = os.path.join(resource_path, "doesnt_exist.bmp") + with pytest.raises(IOError): + sdl2ext.load_img(bad_path) + + # Test exception on bad file type + bad_type = os.path.join(resource_path, "tuffy.ttf") + with pytest.raises(sdl2ext.SDLError): + sdl2ext.load_img(bad_type) + + + @pytest.mark.skipif(not _HASPIL, reason="Pillow library is not installed") + def test_pillow_to_image(self): + # Import an image using Pillow + from PIL import Image + img_path = os.path.join(resource_path, "surfacetest.bmp") + pil_img = Image.open(img_path) + + # Convert the image to an SDL surface and verify it worked + sf = sdl2ext.pillow_to_surface(pil_img) + assert isinstance(sf, surface.SDL_Surface) + self.check_image_contents(sf) + surface.SDL_FreeSurface(sf) + + # Try converting a palette image + palette_img = pil_img.convert("P", palette=Image.WEB) + sfp = sdl2ext.pillow_to_surface(palette_img) + pxformat = sfp.format.contents + assert isinstance(sfp, surface.SDL_Surface) + self.check_image_contents(sfp) + assert pxformat.BytesPerPixel == 4 + surface.SDL_FreeSurface(sfp) + + # Try converting a palette image without ARGB conversion + sfp2 = sdl2ext.pillow_to_surface(palette_img, False) + pxformat = sfp2.format.contents + assert isinstance(sfp2, surface.SDL_Surface) + assert pxformat.BytesPerPixel == 1 + sdl_palette = pxformat.palette.contents + pil_palette = palette_img.getpalette() + assert sdl_palette.colors[0].r == pil_palette[0] + assert sdl_palette.colors[0].g == pil_palette[1] + assert sdl_palette.colors[0].b == pil_palette[2] + surface.SDL_FreeSurface(sfp2) + + # Test loading all supported test images and compare against reference + resources = os.listdir(resource_path) + test_imgs = [f for f in resources if f[:11] == "surfacetest"] + for img in test_imgs: + fmt = img.split(".")[-1] + if fmt in ("webp", "xcf", "lbm", "svg"): + continue + pil_img = Image.open(os.path.join(resource_path, img)) + sf = sdl2ext.pillow_to_surface(pil_img) + assert isinstance(sf, surface.SDL_Surface) + assert sf.format.contents.format == pixels.SDL_PIXELFORMAT_ARGB8888 + if fmt not in skip_color_check: + self.check_image_contents(sf) + surface.SDL_FreeSurface(sf) + def test_load_image(self): - # TODO: add image comparision to check, if it actually does the - # right thing (SDL2 BMP loaded image?) - # Add argument tests - try: - import PIL - _HASPIL = True - except ImportError: - _HASPIL = False - - fname = "surfacetest.%s" - for fmt in formats: - filename = RESOURCES.get_path(fname % fmt) - sf = sdl2ext.load_image(filename) + resources = os.listdir(resource_path) + test_imgs = [f for f in resources if f[:11] == "surfacetest"] + for img in test_imgs: + img_path = os.path.join(resource_path, img) + fmt = img.split(".")[-1] + if fmt in skip_formats: + continue + + # Try normal loading + sf = sdl2ext.load_image(img_path) assert isinstance(sf, surface.SDL_Surface) # Force only PIL if _HASPIL and fmt not in ("webp", "xcf", "lbm", "svg"): - sf = sdl2ext.load_image(filename, enforce="PIL") + sf = sdl2ext.load_image(img_path, enforce="PIL") assert isinstance(sf, surface.SDL_Surface) # Force only sdlimage - sf = sdl2ext.load_image(filename, enforce="SDL") + sf = sdl2ext.load_image(img_path, enforce="SDL") assert isinstance(sf, surface.SDL_Surface) diff --git a/sdl2/test/sdl2ext_gui_test.py b/sdl2/test/sdl2ext_msgbox_test.py similarity index 64% rename from sdl2/test/sdl2ext_gui_test.py rename to sdl2/test/sdl2ext_msgbox_test.py index 3280566e..e38015e5 100644 --- a/sdl2/test/sdl2ext_gui_test.py +++ b/sdl2/test/sdl2ext_msgbox_test.py @@ -1,10 +1,10 @@ -import sys import pytest from sdl2 import SDL_Window from sdl2 import ext as sdl2ext from sdl2 import messagebox as mb -class TestSDL2ExtGUI(object): + +class TestSDL2ExtMsgBox(object): __tags__ = ["sdl", "sdl2ext"] @classmethod @@ -81,71 +81,3 @@ def test_show_messagebox(self): @pytest.mark.skip("not implemented, requires GUI interaction") def test_show_alert(self): pass - - @pytest.mark.skip("not implemented") - def test_UIFactory(self): - pass - - @pytest.mark.skip("not implemented") - def test_UIFactory_create_button(self): - pass - - @pytest.mark.skip("not implemented") - def test_UIFactory_create_checkbutton(self): - pass - - @pytest.mark.skip("not implemented") - def test_UIFactory_create_text_entry(self): - pass - - @pytest.mark.skip("not implemented") - def test_Button(self): - pass - - @pytest.mark.skip("not implemented") - def test_CheckButton(self): - pass - - @pytest.mark.skip("not implemented") - def test_TextEntry(self): - pass - - @pytest.mark.skip("not implemented") - def test_UIProcessor(self): - pass - - @pytest.mark.skip("not implemented") - def test_UIProcessor_activate(self): - pass - - @pytest.mark.skip("not implemented") - def test_UIProcessor_deactivate(self): - pass - - @pytest.mark.skip("not implemented") - def test_UIProcessor_dispatch(self): - pass - - @pytest.mark.skip("not implemented") - def test_UIProcessor_mousedown(self): - pass - - @pytest.mark.skip("not implemented") - def test_UIProcessor_mouseup(self): - pass - - @pytest.mark.skip("not implemented") - def test_UIProcessor_mousemotion(self): - pass - - @pytest.mark.skip("not implemented") - def test_UIProcessor_passevent(self): - pass - - @pytest.mark.skip("not implemented") - def test_UIProcessor_process(self): - pass - - @pytest.mark.skip("not implemented") - def test_UIProcessor_textinput(self): - pass diff --git a/sdl2/test/sdl2ext_pixelaccess_test.py b/sdl2/test/sdl2ext_pixelaccess_test.py index 7e3dea8e..43208cc4 100644 --- a/sdl2/test/sdl2ext_pixelaccess_test.py +++ b/sdl2/test/sdl2ext_pixelaccess_test.py @@ -3,6 +3,7 @@ import pytest from sdl2 import ext as sdl2ext from sdl2.ext import color +from sdl2.ext.surface import _create_surface from sdl2 import surface, pixels try: @@ -12,6 +13,14 @@ _HASNUMPY = False +colors = { + 'red': color.Color(255, 0, 0, 255), + 'blue': color.Color(0, 0, 255, 255), + 'black': color.Color(0, 0, 0, 255), + 'white': color.Color(255, 255, 255, 255) +} + + class TestSDL2ExtPixelAccess(object): __tags__ = ["sdl", "sdl2ext"] @@ -32,67 +41,130 @@ def teardown_class(cls): def test_PixelView(self): - factory = sdl2ext.SpriteFactory(sdl2ext.SOFTWARE) - sprite = factory.create_sprite(size=(10, 10), bpp=32) - view = sdl2ext.PixelView(sprite) - view[1] = (0xAABBCCDD,) * 10 - rcolor = sdl2ext.prepare_color(0xAABBCCDD, sprite) - for index, row in enumerate(view): - if index == 1: - for col in row: - assert col == rcolor - else: - for col in row: - assert col == 0x0 + # Import test image and open pixel view + imgsurf = surface.SDL_LoadBMP(self.testfile.encode("utf-8")) + pxview = sdl2ext.PixelView(imgsurf.contents) + + # Test different coordinates on surface + assert color.ARGB(pxview[0][0]) == colors['red'] + assert color.ARGB(pxview[0][16]) == colors['blue'] + assert color.ARGB(pxview[0][31]) == colors['white'] + assert color.ARGB(pxview[31][31]) == colors['black'] + + # Try modifying surface, test if changes persist + pxview[31][0] = 0xFF808080 # medium grey in ARGB + pxview2 = sdl2ext.PixelView(imgsurf) + assert pxview2[31][0] == 0xFF808080 + + # Test that negative indexing works as expected + assert color.ARGB(pxview[0][-1]) == colors['white'] + assert color.ARGB(pxview[-1][-1]) == colors['black'] + + # Test out-of-bounds exceptions for indices + with pytest.raises(IndexError): + pxview[32][32] + + # Test exception on use with a 3 byte per pixel surface + sf_rgb24 = _create_surface((16, 16), fmt="RGB24") + with pytest.raises(RuntimeError): + sdl2ext.PixelView(sf_rgb24) + + surface.SDL_FreeSurface(sf_rgb24) + surface.SDL_FreeSurface(imgsurf) + @pytest.mark.skipif(not _HASNUMPY, reason="numpy module is not supported") def test_pixels2d(self): - colors = { - 'red': color.Color(255, 0, 0, 255), - 'blue': color.Color(0, 0, 255, 255), - 'black': color.Color(0, 0, 0, 255), - 'white': color.Color(255, 255, 255, 255) - } - # Import test image, convert to RGBA, and open pixels2d view + # Import test image and open pixels2d view imgsurf = surface.SDL_LoadBMP(self.testfile.encode("utf-8")) - with pytest.warns(sdl2ext.compat.ExperimentalWarning): - nparray = sdl2ext.pixels2d(imgsurf.contents, transpose=False) + nparray = sdl2ext.pixels2d(imgsurf.contents, transpose=False) assert nparray.shape == (32, 32) + # Test different coordinates on surface assert color.ARGB(nparray[0][0]) == colors['red'] assert color.ARGB(nparray[0][16]) == colors['blue'] assert color.ARGB(nparray[0][31]) == colors['white'] assert color.ARGB(nparray[31][31]) == colors['black'] + # Try modifying surface, test if changes persist nparray[31][0] = 0xFF808080 # medium grey in ARGB - with pytest.warns(sdl2ext.compat.ExperimentalWarning): - nparray2 = sdl2ext.pixels2d(imgsurf, transpose=False) + nparray2 = sdl2ext.pixels2d(imgsurf, transpose=False) assert nparray2[31][0] == 0xFF808080 + # Test exception on use with a 3 byte per pixel surface + sf_rgb24 = _create_surface((16, 16), fmt="RGB24") + with pytest.raises(RuntimeError): + sdl2ext.pixels2d(sf_rgb24) + + surface.SDL_FreeSurface(sf_rgb24) + surface.SDL_FreeSurface(imgsurf) + @pytest.mark.skipif(not _HASNUMPY, reason="numpy module is not supported") def test_pixels3d(self): - colors = { - 'red': color.Color(255, 0, 0, 255), - 'blue': color.Color(0, 0, 255, 255), - 'black': color.Color(0, 0, 0, 255), - 'white': color.Color(255, 255, 255, 255) - } + # Import test image and convert to RGBA rgba = pixels.SDL_PIXELFORMAT_ABGR8888 - # Import test image, convert to RGBA, and open pixels3d view imgsurf = surface.SDL_LoadBMP(self.testfile.encode("utf-8")) imgsurf = surface.SDL_ConvertSurfaceFormat(imgsurf.contents, rgba, 0) - with pytest.warns(sdl2ext.compat.ExperimentalWarning): - nparray = sdl2ext.pixels3d(imgsurf.contents, transpose=False) + + # Create view and test different coordinates on surface + nparray = sdl2ext.pixels3d(imgsurf.contents, transpose=False) assert nparray.shape == (32, 32, 4) - # Test different coordinates on surface assert color.Color(*nparray[0][0]) == colors['red'] assert color.Color(*nparray[0][16]) == colors['blue'] assert color.Color(*nparray[0][31]) == colors['white'] assert color.Color(*nparray[31][31]) == colors['black'] + + # Create transposed view and test different coordinates on surface + nptrans = sdl2ext.pixels3d(imgsurf.contents, transpose=True) + assert nptrans.shape == (32, 32, 4) + assert color.Color(*nptrans[0][0]) == colors['red'] + assert color.Color(*nptrans[16][0]) == colors['blue'] + assert color.Color(*nptrans[31][0]) == colors['white'] + assert color.Color(*nptrans[31][31]) == colors['black'] + # Try modifying surface, test if changes persist grey = [128, 128, 128, 255] nparray[31][0][:] = grey - with pytest.warns(sdl2ext.compat.ExperimentalWarning): - nparray2 = sdl2ext.pixels3d(imgsurf, transpose=False) + nparray2 = sdl2ext.pixels3d(imgsurf, transpose=False) assert color.Color(*nparray2[31][0]) == color.Color(*grey) + + # Test usage with a 3 bytes-per-pixel surface + sf_rgb24 = _create_surface((16, 16), grey, fmt="RGB24") + nparray_rgb24 = sdl2ext.pixels3d(sf_rgb24) + assert color.Color(*nparray_rgb24[0][0]) == color.Color(*grey) + + surface.SDL_FreeSurface(sf_rgb24) + surface.SDL_FreeSurface(imgsurf) + + + @pytest.mark.skipif(not _HASNUMPY, reason="numpy module is not supported") + def test_surface_to_ndarray(self): + # Import test image & create an RGBA copy + rgba = pixels.SDL_PIXELFORMAT_ABGR8888 + imgsurf = surface.SDL_LoadBMP(self.testfile.encode("utf-8")) + rgbasurf = surface.SDL_ConvertSurfaceFormat(imgsurf.contents, rgba, 0) + + # Create a 2D ndarray from the surface & test different coordinates + arr_2d = sdl2ext.surface_to_ndarray(imgsurf.contents, ndim=2) + assert color.ARGB(arr_2d[0][0]) == colors['red'] + assert color.ARGB(arr_2d[0][16]) == colors['blue'] + assert color.ARGB(arr_2d[0][31]) == colors['white'] + assert color.ARGB(arr_2d[31][31]) == colors['black'] + + # Create a 3D ndarray from the surface & test different coordinates + arr_3d = sdl2ext.surface_to_ndarray(rgbasurf.contents) + assert arr_3d.shape == (32, 32, 4) + assert color.Color(*arr_3d[0][0]) == colors['red'] + assert color.Color(*arr_3d[0][16]) == colors['blue'] + assert color.Color(*arr_3d[0][31]) == colors['white'] + assert color.Color(*arr_3d[31][31]) == colors['black'] + + # Try modifying surface, make sure changes don't persist + grey = [128, 128, 128, 255] + arr_3d[31][0][:] = grey + arr_view = sdl2ext.pixels3d(rgbasurf, transpose=False) + assert color.Color(*arr_view[31][0]) != color.Color(*grey) + + surface.SDL_FreeSurface(imgsurf) + surface.SDL_FreeSurface(rgbasurf) diff --git a/sdl2/test/sdl2ext_renderer_test.py b/sdl2/test/sdl2ext_renderer_test.py new file mode 100644 index 00000000..7390353c --- /dev/null +++ b/sdl2/test/sdl2ext_renderer_test.py @@ -0,0 +1,372 @@ +import sys +import pytest +from ctypes import addressof + +from sdl2 import ext as sdl2ext +from sdl2 import dll +from sdl2.rect import SDL_Point +from sdl2.render import SDL_Renderer, SDL_Texture +from sdl2.surface import SDL_CreateRGBSurface, SDL_FreeSurface + +_ISPYPY = hasattr(sys, "pypy_version_info") + +if _ISPYPY: + import gc + dogc = gc.collect +else: + dogc = lambda: None + + +class TestSDL2ExtRenderer(object): + __tags__ = ["sdl", "sdl2ext"] + + @classmethod + def setup_class(cls): + try: + sdl2ext.init() + except sdl2ext.SDLError: + raise pytest.skip('Video subsystem not supported') + + @classmethod + def teardown_class(cls): + sdl2ext.quit() + + def check_pixels(self, view, w, h, sprite, c1, c2, cx=0, cy=0): + msg = "color mismatch at %d,%d: %d not in %s" + cx = cx + sprite.x + cy = cy + sprite.y + cw, ch = sprite.size + cmy = cy + ch + cmx = cx + cw + for y in range(w): + for x in range(h): + if cy <= y < cmy and cx <= x < cmx: + assert view[y][x] == c1, msg % (x, y, view[y][x], c1) + else: + assert view[y][x] in c2, msg % (x, y, view[y][x], c2) + + def check_areas(self, view, w, h, rects, c1, c2): + def _inarea(x, y, rs): + for r in rs: + if (x >= r[0] and x < (r[0] + r[2]) and + y >= r[1] and y < (r[1] + r[3])): + return True + return False + msg = "color mismatch at %d,%d: %d not in %s" + for y in range(w): + for x in range(h): + if _inarea(x, y, rects): + assert view[y][x] == c1, msg % (x, y, view[y][x], c1) + else: + assert view[y][x] in c2, msg % (x, y, view[y][x], c2) + + def check_lines(self, view, w, h, points, c1, c2): + def _online(x, y, pts): + for p1, p2 in pts: + if sdl2ext.point_on_line(p1, p2, (x, y)): + return True + return False + msg = "color mismatch at %d,%d: %d not in %s" + for y in range(w): + for x in range(h): + if _online(x, y, points): + assert view[y][x] == c1, msg % (x, y, view[y][x], c1) + else: + assert view[y][x] in c2, msg % (x, y, view[y][x], c2) + + def test_Renderer(self): + sf = SDL_CreateRGBSurface(0, 10, 10, 32, 0, 0, 0, 0) + + # Create renderer with SDL_Surface + renderer = sdl2ext.Renderer(sf.contents) + assert addressof(renderer.rendertarget) == addressof(sf.contents) + assert isinstance(renderer.sdlrenderer.contents, SDL_Renderer) + renderer.destroy() + + # Create renderer with SDL_Surface pointer + renderer = sdl2ext.Renderer(sf) + assert renderer.rendertarget == sf + assert isinstance(renderer.sdlrenderer.contents, SDL_Renderer) + renderer.destroy() + + # Create renderer with SoftwareSprite + sprite = sdl2ext.SoftwareSprite(sf.contents, True) + renderer = sdl2ext.Renderer(sprite) + assert renderer.rendertarget == sprite + assert isinstance(renderer.sdlrenderer.contents, SDL_Renderer) + renderer.destroy() + dogc() + + # Create renderer with Window + window = sdl2ext.Window("Test", size=(1, 1)) + renderer = sdl2ext.Renderer(window) + assert renderer.rendertarget == window + assert isinstance(renderer.sdlrenderer.contents, SDL_Renderer) + renderer.destroy() + dogc() + + # Create renderer with SDL_Window + sdlwindow = window.window + renderer = sdl2ext.Renderer(sdlwindow) + assert renderer.rendertarget == sdlwindow + assert isinstance(renderer.sdlrenderer.contents, SDL_Renderer) + renderer.destroy() + del window + + # Test exception on using a destroyed renderer (and random type errors) + with pytest.raises(RuntimeError): + tst = renderer.sdlrenderer + with pytest.raises(TypeError): + sdl2ext.Renderer(None) + with pytest.raises(TypeError): + sdl2ext.Renderer(1234) + with pytest.raises(TypeError): + sdl2ext.Renderer("test") + dogc() + + def test_Texture(self): + # Create renderer and test surface + rendertarget = SDL_CreateRGBSurface(0, 100, 100, 32, 0, 0, 0, 0) + renderer = sdl2ext.Renderer(rendertarget.contents) + sf = SDL_CreateRGBSurface( + 0, 16, 16, 32, 0xFF000000, 0x00FF0000, 0x0000FF00, 0x000000FF + ) + + # Test creation with surface pointer + tx = sdl2ext.Texture(renderer, sf) + assert isinstance(tx.tx.contents, SDL_Texture) + + # Test destruction and associated behaviour + tx.destroy() + with pytest.raises(RuntimeError): + sdl_tx = tx.tx + + # Test creation with surface contents + tx = sdl2ext.Texture(renderer, sf.contents) + assert isinstance(tx.tx.contents, SDL_Texture) + + # Test texture size + assert tx.size == (16, 16) + + # Test exception on bad input + with pytest.raises(TypeError): + sdl2ext.Texture(sf, sf) + with pytest.raises(TypeError): + sdl2ext.Texture(renderer, renderer) + + # Test exception when accessing Texture with destroyed Renderer + renderer.destroy() + with pytest.raises(RuntimeError): + sdl_tx = tx.tx + tx.destroy() # Ensure texture destruction method doesn't error + SDL_FreeSurface(sf) + + def test_Renderer_color(self): + sf = SDL_CreateRGBSurface(0, 10, 10, 32, + 0xFF000000, + 0x00FF0000, + 0x0000FF00, + 0x000000FF) + renderer = sdl2ext.Renderer(sf.contents) + assert isinstance(renderer.color, sdl2ext.Color) + assert renderer.color == sdl2ext.Color(0, 0, 0, 0) + renderer.color = 0x00FF0000 + assert renderer.color == sdl2ext.Color(0xFF, 0, 0, 0) + renderer.clear() + view = sdl2ext.PixelView(sf.contents) + self.check_areas(view, 10, 10, [[0, 0, 10, 10]], 0xFF000000, (0x0,)) + del view + renderer.color = 0xAABBCCDD + assert renderer.color == sdl2ext.Color(0xBB, 0xCC, 0xDD, 0xAA) + renderer.clear() + view = sdl2ext.PixelView(sf.contents) + self.check_areas(view, 10, 10, [[0, 0, 10, 10]], 0xBBCCDDAA, (0x0,)) + del view + renderer.destroy() + SDL_FreeSurface(sf) + dogc() + + @pytest.mark.skip("not implemented") + def test_Renderer_blendmode(self): + pass + + def test_Renderer_clear(self): + sf = SDL_CreateRGBSurface(0, 10, 10, 32, + 0xFF000000, + 0x00FF0000, + 0x0000FF00, + 0x000000FF) + renderer = sdl2ext.Renderer(sf.contents) + assert isinstance(renderer.color, sdl2ext.Color) + assert renderer.color == sdl2ext.Color(0, 0, 0, 0) + renderer.color = 0x00FF0000 + assert renderer.color == sdl2ext.Color(0xFF, 0, 0, 0) + renderer.clear() + view = sdl2ext.PixelView(sf.contents) + self.check_areas(view, 10, 10, [[0, 0, 10, 10]], 0xFF000000, (0x0,)) + del view + renderer.clear(0xAABBCCDD) + assert renderer.color == sdl2ext.Color(0xFF, 0, 0, 0) + view = sdl2ext.PixelView(sf.contents) + self.check_areas(view, 10, 10, [[0, 0, 10, 10]], 0xBBCCDDAA, (0x0,)) + del view + renderer.destroy() + SDL_FreeSurface(sf) + dogc() + + def test_Renderer_copy(self): + # Initialize target surface and renderer + surface = SDL_CreateRGBSurface(0, 128, 128, 32, 0, 0, 0, 0).contents + renderer = sdl2ext.Renderer(surface) + renderer.clear(0xAABBCC) + view = sdl2ext.PixelView(surface) + + # Test copying a Texture without any arguments (should fill surface) + sf = SDL_CreateRGBSurface(0, 16, 16, 32, 0, 0, 0, 0) + sdl2ext.fill(sf, (0, 0, 0, 0)) + tx = sdl2ext.Texture(renderer, sf) + renderer.copy(tx) + renderer.present() + assert view[0][0] == 0 + assert view[127][127] == 0 + + # Test copying a Texture with only location argument + renderer.clear(0xAABBCC) # reset surface + renderer.copy(tx, dstrect=(10, 10)) + renderer.present() + assert view[0][0] == 0xAABBCC + assert view[10][10] == 0 + assert view[25][25] == 0 + assert view[26][26] == 0xAABBCC + + # Test copying a subset of a Texture + renderer.clear(0xAABBCC) # reset surface + renderer.copy(tx, srcrect=(0, 0, 10, 10), dstrect=(10, 10)) + renderer.present() + assert view[0][0] == 0xAABBCC + assert view[10][10] == 0 + assert view[19][19] == 0 + assert view[20][20] == 0xAABBCC + + # Test copying a Texture with location and size + renderer.clear(0xAABBCC) # reset surface + renderer.copy(tx, dstrect=(10, 10, 30, 40)) + renderer.present() + assert view[0][0] == 0xAABBCC + assert view[10][10] == 0 + assert view[49][39] == 0 + assert view[50][40] == 0xAABBCC + + if dll.version > 2005: + # Test copying a Texture with rotation + renderer.clear(0xAABBCC) # reset surface + renderer.copy(tx, dstrect=(32, 32), angle=180, center=(0, 0)) + renderer.present() + assert view[0][0] == 0xAABBCC + assert view[16][16] == 0xFF000000 # Rotation suddenly adds alpha? + assert view[31][31] == 0xFF000000 # Rotation suddenly adds alpha? + assert view[32][32] == 0xAABBCC + + # (legacy): Test copying texture from a SpriteFactory + renderer.clear(0) # reset surface + factory = sdl2ext.SpriteFactory(sdl2ext.TEXTURE, renderer=renderer) + w, h = 32, 32 + sp = factory.from_color(0xFF0000, (w, h)) + sp.x, sp.y = 40, 50 + renderer.copy(sp, (0, 0, w, h), (sp.x, sp.y, w, h)) + self.check_pixels(view, 128, 128, sp, 0xFF0000, (0x0,)) + + del view + + def test_Renderer_draw_line(self): + surface = SDL_CreateRGBSurface(0, 128, 128, 32, 0, 0, 0, 0).contents + sdl2ext.fill(surface, 0x0) + renderer = sdl2ext.Renderer(surface) + renderer.draw_line((20, 10, 20, 86), 0x0000FF) + view = sdl2ext.PixelView(surface) + self.check_lines(view, 128, 128, + [((20, 10), (20, 86))], 0x0000FF, (0x0,)) + del view + + def test_Renderer_draw_point(self): + # Initialize target surface and renderer + surface = SDL_CreateRGBSurface(0, 128, 128, 32, 0, 0, 0, 0).contents + sdl2ext.fill(surface, 0x0) + renderer = sdl2ext.Renderer(surface) + + # Try drawing a single point + renderer.draw_point((1, 1), 0x0000FF) + view = sdl2ext.PixelView(surface) + assert view[1][1] == 0x0000FF + + # Try drawing a single SDL_Point + renderer.draw_point(SDL_Point(3, 3), 0x0000FF) + assert view[3][3] == 0x0000FF + + # Try drawing multiple points + renderer.draw_point([(8, 8), SDL_Point(5, 5)], 0x0000FF) + assert view[5][5] == 0x0000FF + assert view[8][8] == 0x0000FF + + # Try subpixel rendering (if supported) + if dll.version >= 2010: + assert view[6][6] == 0x0 + renderer.draw_point((6.5, 6.5), 0x0000FF) + assert view[6][6] != 0x0 + else: + with pytest.raises(RuntimeError): + renderer.draw_point((6.5, 6.5), 0x0000FF) + del view + + # Test exceptions on bad input + with pytest.raises(ValueError): + renderer.draw_point((0, 0, 2), 0x0000FF) + with pytest.raises(ValueError): + renderer.draw_point([(0, 0), (1, 2, 3)], 0x0000FF) + + + def test_Renderer_draw_rect(self): + surface = SDL_CreateRGBSurface(0, 128, 128, 32, 0, 0, 0, 0).contents + sdl2ext.fill(surface, 0x0) + renderer = sdl2ext.Renderer(surface) + renderer.draw_rect((40, 50, 32, 32), 0x0000FF) + view = sdl2ext.PixelView(surface) + self.check_lines(view, 128, 128, [ + ((40, 50), (71, 50)), + ((40, 50), (40, 81)), + ((40, 81), (71, 81)), + ((71, 50), (71, 81))], 0x0000FF, (0x0,)) + del view + sdl2ext.fill(surface, 0x0) + renderer.draw_rect([(5, 5, 10, 10), (20, 15, 8, 10)], 0x0000FF) + view = sdl2ext.PixelView(surface) + self.check_lines(view, 128, 128, [ + ((5, 5), (14, 5)), + ((5, 5), (5, 14)), + ((5, 14), (14, 14)), + ((14, 5), (14, 14)), + ((20, 15), (27, 15)), + ((20, 15), (20, 24)), + ((20, 24), (27, 24)), + ((27, 15), (27, 24))], 0x0000FF, (0x0,)) + del view + + def test_Renderer_fill(self): + surface = SDL_CreateRGBSurface(0, 128, 128, 32, 0, 0, 0, 0).contents + sdl2ext.fill(surface, 0x0) + renderer = sdl2ext.Renderer(surface) + factory = sdl2ext.SpriteFactory(sdl2ext.TEXTURE, renderer=renderer) + w, h = 32, 32 + sp = factory.from_color(0xFF0000, (w, h)) + sp.x, sp.y = 40, 50 + renderer.fill((sp.x, sp.y, w, h), 0x0000FF) + view = sdl2ext.PixelView(surface) + self.check_pixels(view, 128, 128, sp, 0x0000FF, (0x0,)) + del view + + sdl2ext.fill(surface, 0x0) + renderer.fill([(5, 5, 10, 10), (20, 15, 8, 10)], 0x0000FF) + view = sdl2ext.PixelView(surface) + self.check_areas(view, 128, 128, [(5, 5, 10, 10), (20, 15, 8, 10)], + 0x0000FF, (0x0,)) + del view diff --git a/sdl2/test/sdl2ext_sprite_test.py b/sdl2/test/sdl2ext_sprite_test.py index ce702031..bdde41d7 100644 --- a/sdl2/test/sdl2ext_sprite_test.py +++ b/sdl2/test/sdl2ext_sprite_test.py @@ -1,18 +1,17 @@ import sys import pytest -from ctypes import ArgumentError, POINTER, byref, addressof -from sdl2.ext.resources import Resources +from ctypes import POINTER, byref, addressof + from sdl2 import ext as sdl2ext -from sdl2.surface import SDL_Surface, SDL_CreateRGBSurface, SDL_FreeSurface +from sdl2.render import ( + SDL_Renderer, SDL_CreateWindowAndRenderer, SDL_DestroyRenderer, + SDL_Texture, SDL_CreateTexture +) +from sdl2.surface import SDL_CreateRGBSurface from sdl2.video import SDL_Window, SDL_WINDOW_HIDDEN, SDL_DestroyWindow -from sdl2.render import SDL_Renderer, SDL_CreateWindowAndRenderer, \ - SDL_DestroyRenderer, SDL_CreateTexture, SDL_Texture, \ - SDL_TEXTUREACCESS_STATIC, SDL_TEXTUREACCESS_STREAMING, \ - SDL_TEXTUREACCESS_TARGET -_ISPYPY = hasattr(sys, "pypy_version_info") -RESOURCES = Resources(__file__, "resources") +_ISPYPY = hasattr(sys, "pypy_version_info") if _ISPYPY: import gc @@ -20,6 +19,7 @@ else: dogc = lambda: None + class MSprite(sdl2ext.Sprite): def __init__(self, w=0, h=0): super(MSprite, self).__init__() @@ -44,357 +44,6 @@ def setup_class(cls): def teardown_class(cls): sdl2ext.quit() - def check_pixels(self, view, w, h, sprite, c1, c2, cx=0, cy=0): - msg = "color mismatch at %d,%d: %d not in %s" - cx = cx + sprite.x - cy = cy + sprite.y - cw, ch = sprite.size - cmy = cy + ch - cmx = cx + cw - for y in range(w): - for x in range(h): - if cy <= y < cmy and cx <= x < cmx: - assert view[y][x] == c1, msg % (x, y, view[y][x], c1) - else: - assert view[y][x] in c2, msg % (x, y, view[y][x], c2) - - def check_areas(self, view, w, h, rects, c1, c2): - def _inarea(x, y, rs): - for r in rs: - if (x >= r[0] and x < (r[0] + r[2]) and - y >= r[1] and y < (r[1] + r[3])): - return True - return False - msg = "color mismatch at %d,%d: %d not in %s" - for y in range(w): - for x in range(h): - if _inarea(x, y, rects): - assert view[y][x] == c1, msg % (x, y, view[y][x], c1) - else: - assert view[y][x] in c2, msg % (x, y, view[y][x], c2) - - def check_lines(self, view, w, h, points, c1, c2): - def _online(x, y, pts): - for p1, p2 in pts: - if sdl2ext.point_on_line(p1, p2, (x, y)): - return True - return False - msg = "color mismatch at %d,%d: %d not in %s" - for y in range(w): - for x in range(h): - if _online(x, y, points): - assert view[y][x] == c1, msg % (x, y, view[y][x], c1) - else: - assert view[y][x] in c2, msg % (x, y, view[y][x], c2) - - def test_SpriteFactory(self): - factory = sdl2ext.SpriteFactory(sdl2ext.SOFTWARE) - assert isinstance(factory, sdl2ext.SpriteFactory) - assert factory.default_args == {} - - factory = sdl2ext.SpriteFactory(sdl2ext.SOFTWARE, bananas="tasty") - assert isinstance(factory, sdl2ext.SpriteFactory) - assert factory.default_args == {"bananas": "tasty"} - - window = sdl2ext.Window("Test", size=(1, 1)) - renderer = sdl2ext.Renderer(window) - - factory = sdl2ext.SpriteFactory(sdl2ext.TEXTURE, renderer=renderer) - assert isinstance(factory, sdl2ext.SpriteFactory) - - factory = sdl2ext.SpriteFactory(sdl2ext.TEXTURE, renderer=renderer) - assert isinstance(factory, sdl2ext.SpriteFactory) - assert factory.default_args == {"renderer": renderer} - - with pytest.raises(ValueError): - sdl2ext.SpriteFactory("Test") - with pytest.raises(ValueError): - sdl2ext.SpriteFactory(-456) - with pytest.raises(ValueError): - sdl2ext.SpriteFactory(123) - with pytest.raises(ValueError): - sdl2ext.SpriteFactory(sdl2ext.TEXTURE) - dogc() - - def test_SpriteFactory_create_sprite(self): - window = sdl2ext.Window("Test", size=(1, 1)) - renderer = sdl2ext.Renderer(window) - tfactory = sdl2ext.SpriteFactory(sdl2ext.TEXTURE, renderer=renderer) - sfactory = sdl2ext.SpriteFactory(sdl2ext.SOFTWARE) - - for w in range(0, 100): - for h in range(0, 100): - for bpp in (1, 4, 8, 12, 15, 16, 24, 32): - sprite = sfactory.create_sprite(size=(w, h), bpp=bpp) - assert isinstance(sprite, sdl2ext.SoftwareSprite) - - if w == 0 or h == 0: - with pytest.raises(sdl2ext.SDLError): - tfactory.create_sprite(size=(w, h)) - continue - sprite = tfactory.create_sprite(size=(w, h)) - assert isinstance(sprite, sdl2ext.TextureSprite) - dogc() - - def test_SpriteFactory_create_software_sprite(self): - factory = sdl2ext.SpriteFactory(sdl2ext.SOFTWARE) - for w in range(0, 100): - for h in range(0, 100): - for bpp in (1, 4, 8, 12, 15, 16, 24, 32): - sprite = factory.create_software_sprite((w, h), bpp) - assert isinstance(sprite, sdl2ext.SoftwareSprite) - - #self.assertRaises(ValueError, factory.create_software_sprite, (-1,-1)) - #self.assertRaises(ValueError, factory.create_software_sprite, (-10,5)) - #self.assertRaises(ValueError, factory.create_software_sprite, (10,-5)) - with pytest.raises(TypeError): - factory.create_software_sprite(size=None) - with pytest.raises(sdl2ext.SDLError): - factory.create_software_sprite(size=(10, 10), bpp=-1) - with pytest.raises(TypeError): - factory.create_software_sprite(masks=5) - with pytest.raises((ArgumentError, TypeError)): - factory.create_software_sprite(size=(10, 10), - masks=(None, None, None, None)) - with pytest.raises((ArgumentError, TypeError)): - factory.create_software_sprite(size=(10, 10), - masks=("Test", 1, 2, 3)) - dogc() - - def test_SpriteFactory_create_texture_sprite(self): - window = sdl2ext.Window("Test", size=(1, 1)) - renderer = sdl2ext.Renderer(window) - factory = sdl2ext.SpriteFactory(sdl2ext.TEXTURE, renderer=renderer) - for w in range(1, 100): - for h in range(1, 100): - sprite = factory.create_texture_sprite(renderer, size=(w, h)) - assert isinstance(sprite, sdl2ext.TextureSprite) - del sprite - - # Test different access flags - for flag in (SDL_TEXTUREACCESS_STATIC, SDL_TEXTUREACCESS_STREAMING, - SDL_TEXTUREACCESS_TARGET): - sprite = factory.create_texture_sprite(renderer, size=(64, 64), - access=flag) - assert isinstance(sprite, sdl2ext.TextureSprite) - del sprite - dogc() - - def test_SpriteFactory_from_image(self): - window = sdl2ext.Window("Test", size=(1, 1)) - renderer = sdl2ext.Renderer(window) - tfactory = sdl2ext.SpriteFactory(sdl2ext.TEXTURE, renderer=renderer) - sfactory = sdl2ext.SpriteFactory(sdl2ext.SOFTWARE) - - for suffix in ("tif", "png", "jpg"): - imgname = RESOURCES.get_path("surfacetest.%s" % suffix) - tsprite = tfactory.from_image(imgname) - assert isinstance(tsprite, sdl2ext.TextureSprite) - ssprite = sfactory.from_image(imgname) - assert isinstance(ssprite, sdl2ext.SoftwareSprite) - - for factory in (tfactory, sfactory): - with pytest.raises((ArgumentError, ValueError)): - factory.from_image(None) - #self.assertRaises((IOError, SDLError), - # factory.from_image, "banana") - if not _ISPYPY: - with pytest.raises(ArgumentError): - factory.from_image(12345) - dogc() - - @pytest.mark.skip("not implemented") - def test_SpriteFactory_from_object(self): - window = sdl2ext.Window("Test", size=(1, 1)) - renderer = sdl2ext.Renderer(window) - tfactory = sdl2ext.SpriteFactory(sdl2ext.TEXTURE, renderer=renderer) - sfactory = sdl2ext.SpriteFactory(sdl2ext.SOFTWARE) - - def test_SpriteFactory_from_surface(self): - window = sdl2ext.Window("Test", size=(1, 1)) - renderer = sdl2ext.Renderer(window) - tfactory = sdl2ext.SpriteFactory(sdl2ext.TEXTURE, renderer=renderer) - sfactory = sdl2ext.SpriteFactory(sdl2ext.SOFTWARE) - - sf = SDL_CreateRGBSurface(0, 10, 10, 32, 0, 0, 0, 0) - tsprite = tfactory.from_surface(sf.contents) - assert isinstance(tsprite, sdl2ext.TextureSprite) - ssprite = sfactory.from_surface(sf.contents) - assert isinstance(ssprite, sdl2ext.SoftwareSprite) - SDL_FreeSurface(sf) - - for factory in (tfactory, sfactory): - with pytest.raises((sdl2ext.SDLError, AttributeError, ArgumentError, - TypeError)): - factory.from_surface(None) - with pytest.raises((AttributeError, ArgumentError, TypeError)): - factory.from_surface("test") - # TODO: crashes pypy 2.0 - #self.assertRaises((AttributeError, ArgumentError, TypeError), - # factory.from_surface, 1234) - dogc() - - def test_SpriteFactory_from_text(self): - sfactory = sdl2ext.SpriteFactory(sdl2ext.SOFTWARE) - fm = sdl2ext.FontManager(RESOURCES.get_path("tuffy.ttf")) - - # No Fontmanager passed - with pytest.raises(KeyError): - sfactory.from_text("Test") - - # Passing various keywords arguments - sprite = sfactory.from_text("Test", fontmanager=fm) - assert isinstance(sprite, sdl2ext.SoftwareSprite) - - sprite = sfactory.from_text("Test", fontmanager=fm, alias="tuffy") - assert isinstance(sprite, sdl2ext.SoftwareSprite) - - # Get text from a texture sprite factory - window = sdl2ext.Window("Test", size=(1, 1)) - renderer = sdl2ext.Renderer(window) - tfactory = sdl2ext.SpriteFactory(sdl2ext.TEXTURE, - renderer=renderer, - fontmanager=fm) - sprite = tfactory.from_text("Test", alias="tuffy") - assert isinstance(sprite, sdl2ext.TextureSprite) - dogc() - - def test_SpriteRenderSystem(self): - renderer = sdl2ext.SpriteRenderSystem() - assert isinstance(renderer, sdl2ext.SpriteRenderSystem) - assert renderer.sortfunc is not None - assert sdl2ext.Sprite in renderer.componenttypes - - def test_SpriteRenderSystem_sortfunc(self): - def func(p): - pass - - renderer = sdl2ext.SpriteRenderSystem() - assert renderer.sortfunc is not None - renderer.sortfunc = func - assert renderer.sortfunc == func - - def setf(x, f): - x.sortfunc = f - with pytest.raises(TypeError): - setf(renderer, None) - with pytest.raises(TypeError): - setf(renderer, "Test") - with pytest.raises(TypeError): - setf(renderer, 1234) - - @pytest.mark.skip("not implemented") - def test_SpriteRenderSystem_render(self): - pass - - @pytest.mark.skip("not implemented") - def test_SpriteRenderSystem_process(self): - pass - - def test_SoftwareSpriteRenderSystem(self): - with pytest.raises(TypeError): - sdl2ext.SoftwareSpriteRenderSystem() - with pytest.raises(TypeError): - sdl2ext.SoftwareSpriteRenderSystem(None) - with pytest.raises(TypeError): - sdl2ext.SoftwareSpriteRenderSystem("Test") - with pytest.raises(TypeError): - sdl2ext.SoftwareSpriteRenderSystem(12345) - - window = sdl2ext.Window("Test", size=(1, 1)) - renderer = sdl2ext.SoftwareSpriteRenderSystem(window) - assert isinstance(renderer, sdl2ext.SpriteRenderSystem) - assert renderer.window == window.window - assert isinstance(renderer.surface, SDL_Surface) - - renderer = sdl2ext.SoftwareSpriteRenderSystem(window.window) - assert isinstance(renderer, sdl2ext.SpriteRenderSystem) - assert renderer.window == window.window - assert isinstance(renderer.surface, SDL_Surface) - - assert renderer.sortfunc is not None - assert not (sdl2ext.Sprite in renderer.componenttypes) - assert sdl2ext.SoftwareSprite in renderer.componenttypes - dogc() - - def test_SoftwareSpriteRenderSystem_render(self): - sf1 = SDL_CreateRGBSurface(0, 12, 7, 32, 0, 0, 0, 0) - sp1 = sdl2ext.SoftwareSprite(sf1.contents, True) - sdl2ext.fill(sp1, 0xFF0000) - - sf2 = SDL_CreateRGBSurface(0, 3, 9, 32, 0, 0, 0, 0) - sp2 = sdl2ext.SoftwareSprite(sf2.contents, True) - sdl2ext.fill(sp2, 0x00FF00) - sprites = [sp1, sp2] - - window = sdl2ext.Window("Test", size=(20, 20)) - renderer = sdl2ext.SoftwareSpriteRenderSystem(window) - assert isinstance(renderer, sdl2ext.SpriteRenderSystem) - - with pytest.raises(AttributeError): - renderer.render(None, None, None) - with pytest.raises(AttributeError): - renderer.render([None, None], - None, None) - - for x, y in ((0, 0), (3, 3), (20, 20), (1, 12), (5, 6)): - sp1.position = x, y - renderer.render(sp1) - view = sdl2ext.PixelView(renderer.surface) - self.check_pixels(view, 20, 20, sp1, 0xFF0000, (0x0,)) - del view - sdl2ext.fill(renderer.surface, 0x0) - sp1.position = 0, 0 - sp2.position = 14, 1 - renderer.render(sprites) - view = sdl2ext.PixelView(renderer.surface) - self.check_pixels(view, 20, 20, sp1, 0xFF0000, (0x0, 0x00FF00)) - self.check_pixels(view, 20, 20, sp2, 0x00FF00, (0x0, 0xFF0000)) - del view - sdl2ext.fill(renderer.surface, 0x0) - renderer.render(sprites, 1, 2) - view = sdl2ext.PixelView(renderer.surface) - self.check_pixels(view, 20, 20, sp1, 0xFF0000, (0x0, 0x00FF00), 1, 2) - self.check_pixels(view, 20, 20, sp2, 0x00FF00, (0x0, 0xFF0000), 1, 2) - del view - - def test_SoftwareSpriteRenderSystem_process(self): - sf1 = SDL_CreateRGBSurface(0, 5, 10, 32, 0, 0, 0, 0) - sp1 = sdl2ext.SoftwareSprite(sf1.contents, True) - sp1.depth = 0 - sdl2ext.fill(sp1, 0xFF0000) - - sf2 = SDL_CreateRGBSurface(0, 5, 10, 32, 0, 0, 0, 0) - sp2 = sdl2ext.SoftwareSprite(sf2.contents, True) - sp2.depth = 99 - sdl2ext.fill(sp2, 0x00FF00) - sprites = [sp1, sp2] - - window = sdl2ext.Window("Test", size=(20, 20)) - renderer = sdl2ext.SoftwareSpriteRenderSystem(window) - - renderer.process("fakeworld", sprites) - view = sdl2ext.PixelView(renderer.surface) - # Only sp2 wins, since its depth is higher - self.check_pixels(view, 20, 20, sp1, 0x00FF00, (0x0,)) - self.check_pixels(view, 20, 20, sp2, 0x00FF00, (0x0,)) - del view - - with pytest.raises(TypeError): - renderer.process(None, None) - - @pytest.mark.skip("not implemented") - def test_TextureSpriteRenderSystem(self): - pass - - @pytest.mark.skip("not implemented") - def test_TextureSpriteRenderSystem_render(self): - pass - - @pytest.mark.skip("not implemented") - def test_TextureSpriteRenderSystem_process(self): - pass - def test_Sprite(self): sprite = MSprite() assert isinstance(sprite, MSprite) @@ -551,175 +200,3 @@ def setarea(s, v): SDL_DestroyRenderer(renderer) SDL_DestroyWindow(window) dogc() - - def test_Renderer(self): - sf = SDL_CreateRGBSurface(0, 10, 10, 32, 0, 0, 0, 0) - - # Create renderer with SDL_Surface - renderer = sdl2ext.Renderer(sf.contents) - assert addressof(renderer.rendertarget) == addressof(sf.contents) - assert isinstance(renderer.sdlrenderer.contents, SDL_Renderer) - del renderer - - # Create renderer with SDL_Surface pointer - renderer = sdl2ext.Renderer(sf) - assert renderer.rendertarget == sf - assert isinstance(renderer.sdlrenderer.contents, SDL_Renderer) - del renderer - - # Create renderer with SoftwareSprite - sprite = sdl2ext.SoftwareSprite(sf.contents, True) - renderer = sdl2ext.Renderer(sprite) - assert renderer.rendertarget == sprite - assert isinstance(renderer.sdlrenderer.contents, SDL_Renderer) - del renderer - dogc() - - # Create renderer with Window - window = sdl2ext.Window("Test", size=(1, 1)) - renderer = sdl2ext.Renderer(window) - assert renderer.rendertarget == window - assert isinstance(renderer.sdlrenderer.contents, SDL_Renderer) - del renderer - dogc() - - # Create renderer with SDL_Window - sdlwindow = window.window - renderer = sdl2ext.Renderer(sdlwindow) - assert renderer.rendertarget == sdlwindow - assert isinstance(renderer.sdlrenderer.contents, SDL_Renderer) - del renderer - del window - - with pytest.raises(TypeError): - sdl2ext.Renderer(None) - with pytest.raises(TypeError): - sdl2ext.Renderer(1234) - with pytest.raises(TypeError): - sdl2ext.Renderer("test") - dogc() - - def test_Renderer_color(self): - sf = SDL_CreateRGBSurface(0, 10, 10, 32, - 0xFF000000, - 0x00FF0000, - 0x0000FF00, - 0x000000FF) - renderer = sdl2ext.Renderer(sf.contents) - assert isinstance(renderer.color, sdl2ext.Color) - assert renderer.color == sdl2ext.Color(0, 0, 0, 0) - renderer.color = 0x00FF0000 - assert renderer.color == sdl2ext.Color(0xFF, 0, 0, 0) - renderer.clear() - view = sdl2ext.PixelView(sf.contents) - self.check_areas(view, 10, 10, [[0, 0, 10, 10]], 0xFF000000, (0x0,)) - del view - renderer.color = 0xAABBCCDD - assert renderer.color == sdl2ext.Color(0xBB, 0xCC, 0xDD, 0xAA) - renderer.clear() - view = sdl2ext.PixelView(sf.contents) - self.check_areas(view, 10, 10, [[0, 0, 10, 10]], 0xBBCCDDAA, (0x0,)) - del view - del renderer - SDL_FreeSurface(sf) - dogc() - - @pytest.mark.skip("not implemented") - def test_Renderer_blendmode(self): - pass - - def test_Renderer_clear(self): - sf = SDL_CreateRGBSurface(0, 10, 10, 32, - 0xFF000000, - 0x00FF0000, - 0x0000FF00, - 0x000000FF) - renderer = sdl2ext.Renderer(sf.contents) - assert isinstance(renderer.color, sdl2ext.Color) - assert renderer.color == sdl2ext.Color(0, 0, 0, 0) - renderer.color = 0x00FF0000 - assert renderer.color == sdl2ext.Color(0xFF, 0, 0, 0) - renderer.clear() - view = sdl2ext.PixelView(sf.contents) - self.check_areas(view, 10, 10, [[0, 0, 10, 10]], 0xFF000000, (0x0,)) - del view - renderer.clear(0xAABBCCDD) - assert renderer.color == sdl2ext.Color(0xFF, 0, 0, 0) - view = sdl2ext.PixelView(sf.contents) - self.check_areas(view, 10, 10, [[0, 0, 10, 10]], 0xBBCCDDAA, (0x0,)) - del view - del renderer - SDL_FreeSurface(sf) - dogc() - - def test_Renderer_copy(self): - surface = SDL_CreateRGBSurface(0, 128, 128, 32, 0, 0, 0, 0).contents - sdl2ext.fill(surface, 0x0) - renderer = sdl2ext.Renderer(surface) - factory = sdl2ext.SpriteFactory(sdl2ext.TEXTURE, renderer=renderer) - w, h = 32, 32 - sp = factory.from_color(0xFF0000, (w, h)) - sp.x, sp.y = 40, 50 - renderer.copy(sp, (0, 0, w, h), (sp.x, sp.y, w, h)) - view = sdl2ext.PixelView(surface) - self.check_pixels(view, 128, 128, sp, 0xFF0000, (0x0,)) - del view - - def test_Renderer_draw_line(self): - surface = SDL_CreateRGBSurface(0, 128, 128, 32, 0, 0, 0, 0).contents - sdl2ext.fill(surface, 0x0) - renderer = sdl2ext.Renderer(surface) - renderer.draw_line((20, 10, 20, 86), 0x0000FF) - view = sdl2ext.PixelView(surface) - self.check_lines(view, 128, 128, - [((20, 10), (20, 86))], 0x0000FF, (0x0,)) - del view - - @pytest.mark.skip("not implemented") - def test_Renderer_draw_point(self): - pass - - def test_Renderer_draw_rect(self): - surface = SDL_CreateRGBSurface(0, 128, 128, 32, 0, 0, 0, 0).contents - sdl2ext.fill(surface, 0x0) - renderer = sdl2ext.Renderer(surface) - renderer.draw_rect((40, 50, 32, 32), 0x0000FF) - view = sdl2ext.PixelView(surface) - self.check_lines(view, 128, 128, [ - ((40, 50), (71, 50)), - ((40, 50), (40, 81)), - ((40, 81), (71, 81)), - ((71, 50), (71, 81))], 0x0000FF, (0x0,)) - del view - sdl2ext.fill(surface, 0x0) - renderer.draw_rect([(5, 5, 10, 10), (20, 15, 8, 10)], 0x0000FF) - view = sdl2ext.PixelView(surface) - self.check_lines(view, 128, 128, [ - ((5, 5), (14, 5)), - ((5, 5), (5, 14)), - ((5, 14), (14, 14)), - ((14, 5), (14, 14)), - ((20, 15), (27, 15)), - ((20, 15), (20, 24)), - ((20, 24), (27, 24)), - ((27, 15), (27, 24))], 0x0000FF, (0x0,)) - del view - - def test_Renderer_fill(self): - surface = SDL_CreateRGBSurface(0, 128, 128, 32, 0, 0, 0, 0).contents - sdl2ext.fill(surface, 0x0) - renderer = sdl2ext.Renderer(surface) - factory = sdl2ext.SpriteFactory(sdl2ext.TEXTURE, renderer=renderer) - w, h = 32, 32 - sp = factory.from_color(0xFF0000, (w, h)) - sp.x, sp.y = 40, 50 - renderer.fill((sp.x, sp.y, w, h), 0x0000FF) - view = sdl2ext.PixelView(surface) - self.check_pixels(view, 128, 128, sp, 0x0000FF, (0x0,)) - del view - sdl2ext.fill(surface, 0x0) - renderer.fill([(5, 5, 10, 10), (20, 15, 8, 10)], 0x0000FF) - view = sdl2ext.PixelView(surface) - self.check_areas(view, 128, 128, [(5, 5, 10, 10), (20, 15, 8, 10)], - 0x0000FF, (0x0,)) - del view diff --git a/sdl2/test/sdl2ext_spritesystem_test.py b/sdl2/test/sdl2ext_spritesystem_test.py new file mode 100644 index 00000000..7cfeb36c --- /dev/null +++ b/sdl2/test/sdl2ext_spritesystem_test.py @@ -0,0 +1,356 @@ +import sys +import pytest +from ctypes import ArgumentError + +from sdl2 import ext as sdl2ext +from sdl2.ext.resources import Resources +from sdl2.render import ( + SDL_TEXTUREACCESS_STATIC, SDL_TEXTUREACCESS_STREAMING, SDL_TEXTUREACCESS_TARGET +) +from sdl2.surface import SDL_Surface, SDL_CreateRGBSurface, SDL_FreeSurface + + +_ISPYPY = hasattr(sys, "pypy_version_info") + + +RESOURCES = Resources(__file__, "resources") + +if _ISPYPY: + import gc + dogc = gc.collect +else: + dogc = lambda: None + + +class TestSDL2ExtSpriteSystem(object): + __tags__ = ["sdl", "sdl2ext"] + + @classmethod + def setup_class(cls): + try: + sdl2ext.init() + except sdl2ext.SDLError: + raise pytest.skip('Video subsystem not supported') + + @classmethod + def teardown_class(cls): + sdl2ext.quit() + + def check_pixels(self, view, w, h, sprite, c1, c2, cx=0, cy=0): + msg = "color mismatch at %d,%d: %d not in %s" + cx = cx + sprite.x + cy = cy + sprite.y + cw, ch = sprite.size + cmy = cy + ch + cmx = cx + cw + for y in range(w): + for x in range(h): + if cy <= y < cmy and cx <= x < cmx: + assert view[y][x] == c1, msg % (x, y, view[y][x], c1) + else: + assert view[y][x] in c2, msg % (x, y, view[y][x], c2) + + def test_SpriteFactory(self): + factory = sdl2ext.SpriteFactory(sdl2ext.SOFTWARE) + assert isinstance(factory, sdl2ext.SpriteFactory) + assert factory.default_args == {} + + factory = sdl2ext.SpriteFactory(sdl2ext.SOFTWARE, bananas="tasty") + assert isinstance(factory, sdl2ext.SpriteFactory) + assert factory.default_args == {"bananas": "tasty"} + + window = sdl2ext.Window("Test", size=(1, 1)) + renderer = sdl2ext.Renderer(window) + + factory = sdl2ext.SpriteFactory(sdl2ext.TEXTURE, renderer=renderer) + assert isinstance(factory, sdl2ext.SpriteFactory) + + factory = sdl2ext.SpriteFactory(sdl2ext.TEXTURE, renderer=renderer) + assert isinstance(factory, sdl2ext.SpriteFactory) + assert factory.default_args == {"renderer": renderer} + + with pytest.raises(ValueError): + sdl2ext.SpriteFactory("Test") + with pytest.raises(ValueError): + sdl2ext.SpriteFactory(-456) + with pytest.raises(ValueError): + sdl2ext.SpriteFactory(123) + with pytest.raises(ValueError): + sdl2ext.SpriteFactory(sdl2ext.TEXTURE) + dogc() + + def test_SpriteFactory_create_sprite(self): + window = sdl2ext.Window("Test", size=(1, 1)) + renderer = sdl2ext.Renderer(window) + tfactory = sdl2ext.SpriteFactory(sdl2ext.TEXTURE, renderer=renderer) + sfactory = sdl2ext.SpriteFactory(sdl2ext.SOFTWARE) + + for w in range(0, 100): + for h in range(0, 100): + for bpp in (1, 4, 8, 12, 15, 16, 24, 32): + sprite = sfactory.create_sprite(size=(w, h), bpp=bpp) + assert isinstance(sprite, sdl2ext.SoftwareSprite) + + if w == 0 or h == 0: + with pytest.raises(sdl2ext.SDLError): + tfactory.create_sprite(size=(w, h)) + continue + sprite = tfactory.create_sprite(size=(w, h)) + assert isinstance(sprite, sdl2ext.TextureSprite) + dogc() + + def test_SpriteFactory_create_software_sprite(self): + factory = sdl2ext.SpriteFactory(sdl2ext.SOFTWARE) + for w in range(0, 100): + for h in range(0, 100): + for bpp in (1, 4, 8, 12, 15, 16, 24, 32): + sprite = factory.create_software_sprite((w, h), bpp) + assert isinstance(sprite, sdl2ext.SoftwareSprite) + + #self.assertRaises(ValueError, factory.create_software_sprite, (-1,-1)) + #self.assertRaises(ValueError, factory.create_software_sprite, (-10,5)) + #self.assertRaises(ValueError, factory.create_software_sprite, (10,-5)) + with pytest.raises(TypeError): + factory.create_software_sprite(size=None) + with pytest.raises(sdl2ext.SDLError): + factory.create_software_sprite(size=(10, 10), bpp=-1) + with pytest.raises(TypeError): + factory.create_software_sprite(masks=5) + with pytest.raises((ArgumentError, TypeError)): + factory.create_software_sprite(size=(10, 10), + masks=(None, None, None, None)) + with pytest.raises((ArgumentError, TypeError)): + factory.create_software_sprite(size=(10, 10), + masks=("Test", 1, 2, 3)) + dogc() + + def test_SpriteFactory_create_texture_sprite(self): + window = sdl2ext.Window("Test", size=(1, 1)) + renderer = sdl2ext.Renderer(window) + factory = sdl2ext.SpriteFactory(sdl2ext.TEXTURE, renderer=renderer) + for w in range(1, 100): + for h in range(1, 100): + sprite = factory.create_texture_sprite(renderer, size=(w, h)) + assert isinstance(sprite, sdl2ext.TextureSprite) + del sprite + + # Test different access flags + for flag in (SDL_TEXTUREACCESS_STATIC, SDL_TEXTUREACCESS_STREAMING, + SDL_TEXTUREACCESS_TARGET): + sprite = factory.create_texture_sprite(renderer, size=(64, 64), + access=flag) + assert isinstance(sprite, sdl2ext.TextureSprite) + del sprite + dogc() + + def test_SpriteFactory_from_image(self): + window = sdl2ext.Window("Test", size=(1, 1)) + renderer = sdl2ext.Renderer(window) + tfactory = sdl2ext.SpriteFactory(sdl2ext.TEXTURE, renderer=renderer) + sfactory = sdl2ext.SpriteFactory(sdl2ext.SOFTWARE) + + for suffix in ("tif", "png", "jpg"): + imgname = RESOURCES.get_path("surfacetest.%s" % suffix) + tsprite = tfactory.from_image(imgname) + assert isinstance(tsprite, sdl2ext.TextureSprite) + ssprite = sfactory.from_image(imgname) + assert isinstance(ssprite, sdl2ext.SoftwareSprite) + + for factory in (tfactory, sfactory): + with pytest.raises((ArgumentError, ValueError)): + factory.from_image(None) + with pytest.raises((ArgumentError, ValueError)): + factory.from_image(12345) + dogc() + + @pytest.mark.skip("not implemented") + def test_SpriteFactory_from_object(self): + window = sdl2ext.Window("Test", size=(1, 1)) + renderer = sdl2ext.Renderer(window) + tfactory = sdl2ext.SpriteFactory(sdl2ext.TEXTURE, renderer=renderer) + sfactory = sdl2ext.SpriteFactory(sdl2ext.SOFTWARE) + + def test_SpriteFactory_from_surface(self): + window = sdl2ext.Window("Test", size=(1, 1)) + renderer = sdl2ext.Renderer(window) + tfactory = sdl2ext.SpriteFactory(sdl2ext.TEXTURE, renderer=renderer) + sfactory = sdl2ext.SpriteFactory(sdl2ext.SOFTWARE) + + sf = SDL_CreateRGBSurface(0, 10, 10, 32, 0, 0, 0, 0) + tsprite = tfactory.from_surface(sf.contents) + assert isinstance(tsprite, sdl2ext.TextureSprite) + ssprite = sfactory.from_surface(sf.contents) + assert isinstance(ssprite, sdl2ext.SoftwareSprite) + SDL_FreeSurface(sf) + + for factory in (tfactory, sfactory): + with pytest.raises((sdl2ext.SDLError, AttributeError, ArgumentError, + TypeError)): + factory.from_surface(None) + with pytest.raises((AttributeError, ArgumentError, TypeError)): + factory.from_surface("test") + # TODO: crashes pypy 2.0 + #self.assertRaises((AttributeError, ArgumentError, TypeError), + # factory.from_surface, 1234) + dogc() + + def test_SpriteFactory_from_text(self): + sfactory = sdl2ext.SpriteFactory(sdl2ext.SOFTWARE) + fm = sdl2ext.FontManager(RESOURCES.get_path("tuffy.ttf")) + + # No Fontmanager passed + with pytest.raises(KeyError): + sfactory.from_text("Test") + + # Passing various keywords arguments + sprite = sfactory.from_text("Test", fontmanager=fm) + assert isinstance(sprite, sdl2ext.SoftwareSprite) + + sprite = sfactory.from_text("Test", fontmanager=fm, alias="tuffy") + assert isinstance(sprite, sdl2ext.SoftwareSprite) + + # Get text from a texture sprite factory + window = sdl2ext.Window("Test", size=(1, 1)) + renderer = sdl2ext.Renderer(window) + tfactory = sdl2ext.SpriteFactory(sdl2ext.TEXTURE, + renderer=renderer, + fontmanager=fm) + sprite = tfactory.from_text("Test", alias="tuffy") + assert isinstance(sprite, sdl2ext.TextureSprite) + dogc() + + def test_SpriteRenderSystem(self): + renderer = sdl2ext.SpriteRenderSystem() + assert isinstance(renderer, sdl2ext.SpriteRenderSystem) + assert renderer.sortfunc is not None + assert sdl2ext.Sprite in renderer.componenttypes + + def test_SpriteRenderSystem_sortfunc(self): + def func(p): + pass + + renderer = sdl2ext.SpriteRenderSystem() + assert renderer.sortfunc is not None + renderer.sortfunc = func + assert renderer.sortfunc == func + + def setf(x, f): + x.sortfunc = f + with pytest.raises(TypeError): + setf(renderer, None) + with pytest.raises(TypeError): + setf(renderer, "Test") + with pytest.raises(TypeError): + setf(renderer, 1234) + + @pytest.mark.skip("not implemented") + def test_SpriteRenderSystem_render(self): + pass + + @pytest.mark.skip("not implemented") + def test_SpriteRenderSystem_process(self): + pass + + def test_SoftwareSpriteRenderSystem(self): + with pytest.raises(TypeError): + sdl2ext.SoftwareSpriteRenderSystem() + with pytest.raises(TypeError): + sdl2ext.SoftwareSpriteRenderSystem(None) + with pytest.raises(TypeError): + sdl2ext.SoftwareSpriteRenderSystem("Test") + with pytest.raises(TypeError): + sdl2ext.SoftwareSpriteRenderSystem(12345) + + window = sdl2ext.Window("Test", size=(1, 1)) + renderer = sdl2ext.SoftwareSpriteRenderSystem(window) + assert isinstance(renderer, sdl2ext.SpriteRenderSystem) + assert renderer.window == window.window + assert isinstance(renderer.surface, SDL_Surface) + + renderer = sdl2ext.SoftwareSpriteRenderSystem(window.window) + assert isinstance(renderer, sdl2ext.SpriteRenderSystem) + assert renderer.window == window.window + assert isinstance(renderer.surface, SDL_Surface) + + assert renderer.sortfunc is not None + assert not (sdl2ext.Sprite in renderer.componenttypes) + assert sdl2ext.SoftwareSprite in renderer.componenttypes + dogc() + + def test_SoftwareSpriteRenderSystem_render(self): + sf1 = SDL_CreateRGBSurface(0, 12, 7, 32, 0, 0, 0, 0) + sp1 = sdl2ext.SoftwareSprite(sf1.contents, True) + sdl2ext.fill(sp1, 0xFF0000) + + sf2 = SDL_CreateRGBSurface(0, 3, 9, 32, 0, 0, 0, 0) + sp2 = sdl2ext.SoftwareSprite(sf2.contents, True) + sdl2ext.fill(sp2, 0x00FF00) + sprites = [sp1, sp2] + + window = sdl2ext.Window("Test", size=(20, 20)) + renderer = sdl2ext.SoftwareSpriteRenderSystem(window) + assert isinstance(renderer, sdl2ext.SpriteRenderSystem) + + with pytest.raises(AttributeError): + renderer.render(None, None, None) + with pytest.raises(AttributeError): + renderer.render([None, None], + None, None) + + for x, y in ((0, 0), (3, 3), (20, 20), (1, 12), (5, 6)): + sp1.position = x, y + renderer.render(sp1) + view = sdl2ext.PixelView(renderer.surface) + self.check_pixels(view, 20, 20, sp1, 0xFF0000, (0x0,)) + del view + sdl2ext.fill(renderer.surface, 0x0) + sp1.position = 0, 0 + sp2.position = 14, 1 + renderer.render(sprites) + view = sdl2ext.PixelView(renderer.surface) + self.check_pixels(view, 20, 20, sp1, 0xFF0000, (0x0, 0x00FF00)) + self.check_pixels(view, 20, 20, sp2, 0x00FF00, (0x0, 0xFF0000)) + del view + sdl2ext.fill(renderer.surface, 0x0) + renderer.render(sprites, 1, 2) + view = sdl2ext.PixelView(renderer.surface) + self.check_pixels(view, 20, 20, sp1, 0xFF0000, (0x0, 0x00FF00), 1, 2) + self.check_pixels(view, 20, 20, sp2, 0x00FF00, (0x0, 0xFF0000), 1, 2) + del view + + def test_SoftwareSpriteRenderSystem_process(self): + sf1 = SDL_CreateRGBSurface(0, 5, 10, 32, 0, 0, 0, 0) + sp1 = sdl2ext.SoftwareSprite(sf1.contents, True) + sp1.depth = 0 + sdl2ext.fill(sp1, 0xFF0000) + + sf2 = SDL_CreateRGBSurface(0, 5, 10, 32, 0, 0, 0, 0) + sp2 = sdl2ext.SoftwareSprite(sf2.contents, True) + sp2.depth = 99 + sdl2ext.fill(sp2, 0x00FF00) + sprites = [sp1, sp2] + + window = sdl2ext.Window("Test", size=(20, 20)) + renderer = sdl2ext.SoftwareSpriteRenderSystem(window) + + renderer.process("fakeworld", sprites) + view = sdl2ext.PixelView(renderer.surface) + # Only sp2 wins, since its depth is higher + self.check_pixels(view, 20, 20, sp1, 0x00FF00, (0x0,)) + self.check_pixels(view, 20, 20, sp2, 0x00FF00, (0x0,)) + del view + + with pytest.raises(TypeError): + renderer.process(None, None) + + @pytest.mark.skip("not implemented") + def test_TextureSpriteRenderSystem(self): + pass + + @pytest.mark.skip("not implemented") + def test_TextureSpriteRenderSystem_render(self): + pass + + @pytest.mark.skip("not implemented") + def test_TextureSpriteRenderSystem_process(self): + pass diff --git a/sdl2/test/sdl2ext_surface_test.py b/sdl2/test/sdl2ext_surface_test.py new file mode 100644 index 00000000..28c78390 --- /dev/null +++ b/sdl2/test/sdl2ext_surface_test.py @@ -0,0 +1,44 @@ +import pytest +from sdl2.surface import SDL_CreateRGBSurface, SDL_FillRect +from sdl2.rect import SDL_Rect +from sdl2.ext.draw import prepare_color, fill +from sdl2 import ext as sdl2ext + + +class TestSDL2ExtSurface(object): + __tags__ = ["sdl", "sdl2ext"] + + @classmethod + def setup_class(cls): + try: + sdl2ext.init() + except sdl2ext.SDLError: + raise pytest.skip('Video subsystem not supported') + + @classmethod + def teardown_class(cls): + sdl2ext.quit() + + def test_subsurface(self): + # Initialize colour and surface/view + sf = SDL_CreateRGBSurface(0, 10, 10, 32, 0, 0, 0, 0) + WHITE = prepare_color((255, 255, 255), sf) + + # Test creation of subsurface from parent + ssf = sdl2ext.subsurface(sf.contents, (0, 0, 5, 5)) + assert ssf.w == 5 and ssf.h == 5 + + # Test shared pixels between surface + fill(ssf, (255, 255, 255)) + view = sdl2ext.pixels3d(sf.contents, False) + assert all(x == 255 for x in view[0][0][:3]) + assert all(x == 255 for x in view[4][4][:3]) + assert all(x == 0 for x in view[5][5][:3]) + + # Test exceptions on bad input + with pytest.raises(TypeError): + sdl2ext.subsurface(WHITE, (0, 0, 5, 5)) + with pytest.raises(TypeError): + sdl2ext.subsurface(sf, (0, 0, 5)) + with pytest.raises(ValueError): + sdl2ext.subsurface(sf, (0, 0, 50, 50)) diff --git a/sdl2/test/sdl2ext_uisystem_test.py b/sdl2/test/sdl2ext_uisystem_test.py new file mode 100644 index 00000000..be496477 --- /dev/null +++ b/sdl2/test/sdl2ext_uisystem_test.py @@ -0,0 +1,84 @@ +import pytest +from sdl2 import ext as sdl2ext + +class TestSDL2ExtGUI(object): + __tags__ = ["sdl", "sdl2ext"] + + @classmethod + def setup_class(cls): + try: + sdl2ext.init() + except sdl2ext.SDLError: + raise pytest.skip('Video subsystem not supported') + + @classmethod + def teardown_class(cls): + sdl2ext.quit() + + @pytest.mark.skip("not implemented") + def test_UIFactory(self): + pass + + @pytest.mark.skip("not implemented") + def test_UIFactory_create_button(self): + pass + + @pytest.mark.skip("not implemented") + def test_UIFactory_create_checkbutton(self): + pass + + @pytest.mark.skip("not implemented") + def test_UIFactory_create_text_entry(self): + pass + + @pytest.mark.skip("not implemented") + def test_Button(self): + pass + + @pytest.mark.skip("not implemented") + def test_CheckButton(self): + pass + + @pytest.mark.skip("not implemented") + def test_TextEntry(self): + pass + + @pytest.mark.skip("not implemented") + def test_UIProcessor(self): + pass + + @pytest.mark.skip("not implemented") + def test_UIProcessor_activate(self): + pass + + @pytest.mark.skip("not implemented") + def test_UIProcessor_deactivate(self): + pass + + @pytest.mark.skip("not implemented") + def test_UIProcessor_dispatch(self): + pass + + @pytest.mark.skip("not implemented") + def test_UIProcessor_mousedown(self): + pass + + @pytest.mark.skip("not implemented") + def test_UIProcessor_mouseup(self): + pass + + @pytest.mark.skip("not implemented") + def test_UIProcessor_mousemotion(self): + pass + + @pytest.mark.skip("not implemented") + def test_UIProcessor_passevent(self): + pass + + @pytest.mark.skip("not implemented") + def test_UIProcessor_process(self): + pass + + @pytest.mark.skip("not implemented") + def test_UIProcessor_textinput(self): + pass diff --git a/sdl2/video.py b/sdl2/video.py index c112be4b..2e06e05d 100644 --- a/sdl2/video.py +++ b/sdl2/video.py @@ -397,7 +397,7 @@ class SDL_Window(c_void_p): SDL_GetWindowGrab = _bind("SDL_GetWindowGrab", [POINTER(SDL_Window)], SDL_bool) SDL_GetWindowKeyboardGrab = _bind("SDL_GetWindowKeyboardGrab", [POINTER(SDL_Window)], SDL_bool, added='2.0.16') SDL_GetWindowMouseGrab = _bind("SDL_GetWindowMouseGrab", [POINTER(SDL_Window)], SDL_bool, added='2.0.16') -SDL_GetGrabbedWindow = _bind("SDL_GetGrabbedWindow", None, POINTER(SDL_Window)) +SDL_GetGrabbedWindow = _bind("SDL_GetGrabbedWindow", None, POINTER(SDL_Window), added='2.0.4') SDL_SetWindowMouseRect = _bind("SDL_SetWindowMouseRect", [POINTER(SDL_Window), POINTER(SDL_Rect)], c_int, added='2.0.18') SDL_GetWindowMouseRect = _bind("SDL_GetWindowMouseRect", [POINTER(SDL_Window)], POINTER(SDL_Rect), added='2.0.18') SDL_SetWindowBrightness = _bind("SDL_SetWindowBrightness", [POINTER(SDL_Window), c_float], c_int) @@ -409,15 +409,15 @@ class SDL_Window(c_void_p): SDL_IsScreenSaverEnabled = _bind("SDL_IsScreenSaverEnabled", None, SDL_bool) SDL_EnableScreenSaver = _bind("SDL_EnableScreenSaver") SDL_DisableScreenSaver = _bind("SDL_DisableScreenSaver") -SDL_SetWindowHitTest = _bind("SDL_SetWindowHitTest", [POINTER(SDL_Window), SDL_HitTest, c_void_p], c_int) -SDL_GetDisplayDPI = _bind("SDL_GetDisplayDPI", [c_int, POINTER(c_float), POINTER(c_float), POINTER(c_float)], c_int) -SDL_GetDisplayUsableBounds = _bind("SDL_GetDisplayUsableBounds", [c_int, POINTER(SDL_Rect)], c_int) -SDL_GetWindowBordersSize = _bind("SDL_GetWindowBordersSize", [POINTER(SDL_Window), POINTER(c_int), POINTER(c_int), POINTER(c_int), POINTER(c_int)], c_int) -SDL_GetWindowOpacity = _bind("SDL_GetWindowOpacity", [POINTER(SDL_Window), POINTER(c_float)], c_int) -SDL_SetWindowOpacity = _bind("SDL_SetWindowOpacity", [POINTER(SDL_Window), c_float], c_int) -SDL_SetWindowInputFocus = _bind("SDL_SetWindowInputFocus", [POINTER(SDL_Window)], c_int) -SDL_SetWindowModalFor = _bind("SDL_SetWindowModalFor", [POINTER(SDL_Window), POINTER(SDL_Window)], c_int) -SDL_SetWindowResizable = _bind("SDL_SetWindowResizable", [POINTER(SDL_Window), SDL_bool]) +SDL_SetWindowHitTest = _bind("SDL_SetWindowHitTest", [POINTER(SDL_Window), SDL_HitTest, c_void_p], c_int, added='2.0.4') +SDL_GetDisplayDPI = _bind("SDL_GetDisplayDPI", [c_int, POINTER(c_float), POINTER(c_float), POINTER(c_float)], c_int, added='2.0.4') +SDL_GetDisplayUsableBounds = _bind("SDL_GetDisplayUsableBounds", [c_int, POINTER(SDL_Rect)], c_int, added='2.0.5') +SDL_GetWindowBordersSize = _bind("SDL_GetWindowBordersSize", [POINTER(SDL_Window), POINTER(c_int), POINTER(c_int), POINTER(c_int), POINTER(c_int)], c_int, added='2.0.5') +SDL_GetWindowOpacity = _bind("SDL_GetWindowOpacity", [POINTER(SDL_Window), POINTER(c_float)], c_int, added='2.0.5') +SDL_SetWindowOpacity = _bind("SDL_SetWindowOpacity", [POINTER(SDL_Window), c_float], c_int, added='2.0.5') +SDL_SetWindowInputFocus = _bind("SDL_SetWindowInputFocus", [POINTER(SDL_Window)], c_int, added='2.0.5') +SDL_SetWindowModalFor = _bind("SDL_SetWindowModalFor", [POINTER(SDL_Window), POINTER(SDL_Window)], c_int, added='2.0.5') +SDL_SetWindowResizable = _bind("SDL_SetWindowResizable", [POINTER(SDL_Window), SDL_bool], added='2.0.5') SDL_SetWindowAlwaysOnTop = _bind("SDL_SetWindowAlwaysOnTop", [POINTER(SDL_Window), SDL_bool], added='2.0.16') SDL_GL_LoadLibrary = _bind("SDL_GL_LoadLibrary", [c_char_p], c_int)