diff --git a/binaries/org.eclipse.swt.cocoa.macosx.aarch64/.settings/.api_filters b/binaries/org.eclipse.swt.cocoa.macosx.aarch64/.settings/.api_filters index a7c6971eeba..df8e082a29a 100644 --- a/binaries/org.eclipse.swt.cocoa.macosx.aarch64/.settings/.api_filters +++ b/binaries/org.eclipse.swt.cocoa.macosx.aarch64/.settings/.api_filters @@ -46,6 +46,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/binaries/org.eclipse.swt.cocoa.macosx.x86_64/.settings/.api_filters b/binaries/org.eclipse.swt.cocoa.macosx.x86_64/.settings/.api_filters index ace860e8489..d707aac28af 100644 --- a/binaries/org.eclipse.swt.cocoa.macosx.x86_64/.settings/.api_filters +++ b/binaries/org.eclipse.swt.cocoa.macosx.x86_64/.settings/.api_filters @@ -46,6 +46,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/binaries/org.eclipse.swt.gtk.linux.aarch64/.settings/.api_filters b/binaries/org.eclipse.swt.gtk.linux.aarch64/.settings/.api_filters index 445157d2fb4..18bc4bb4553 100644 --- a/binaries/org.eclipse.swt.gtk.linux.aarch64/.settings/.api_filters +++ b/binaries/org.eclipse.swt.gtk.linux.aarch64/.settings/.api_filters @@ -46,6 +46,26 @@ + + + + + + + + + + + + + + + + + + + + @@ -234,6 +254,20 @@ + + + + + + + + + + + + + + diff --git a/binaries/org.eclipse.swt.gtk.linux.ppc64le/.settings/.api_filters b/binaries/org.eclipse.swt.gtk.linux.ppc64le/.settings/.api_filters index cf85cec583a..98a1fe5b74b 100644 --- a/binaries/org.eclipse.swt.gtk.linux.ppc64le/.settings/.api_filters +++ b/binaries/org.eclipse.swt.gtk.linux.ppc64le/.settings/.api_filters @@ -46,6 +46,26 @@ + + + + + + + + + + + + + + + + + + + + @@ -234,6 +254,20 @@ + + + + + + + + + + + + + + diff --git a/binaries/org.eclipse.swt.gtk.linux.riscv64/.settings/.api_filters b/binaries/org.eclipse.swt.gtk.linux.riscv64/.settings/.api_filters index 95adbe43e14..d5286b8c891 100644 --- a/binaries/org.eclipse.swt.gtk.linux.riscv64/.settings/.api_filters +++ b/binaries/org.eclipse.swt.gtk.linux.riscv64/.settings/.api_filters @@ -46,6 +46,20 @@ + + + + + + + + + + + + + + @@ -234,6 +248,14 @@ + + + + + + + + diff --git a/binaries/org.eclipse.swt.gtk.linux.x86_64/.settings/.api_filters b/binaries/org.eclipse.swt.gtk.linux.x86_64/.settings/.api_filters index e8ceeb035e0..fa82c19ef14 100644 --- a/binaries/org.eclipse.swt.gtk.linux.x86_64/.settings/.api_filters +++ b/binaries/org.eclipse.swt.gtk.linux.x86_64/.settings/.api_filters @@ -39,6 +39,12 @@ + + + + + + @@ -234,6 +240,20 @@ + + + + + + + + + + + + + + diff --git a/binaries/org.eclipse.swt.win32.win32.aarch64/.settings/.api_filters b/binaries/org.eclipse.swt.win32.win32.aarch64/.settings/.api_filters index ac0e9574b93..13cc405d17d 100644 --- a/binaries/org.eclipse.swt.win32.win32.aarch64/.settings/.api_filters +++ b/binaries/org.eclipse.swt.win32.win32.aarch64/.settings/.api_filters @@ -148,6 +148,20 @@ + + + + + + + + + + + + + + @@ -336,6 +350,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/binaries/org.eclipse.swt.win32.win32.x86_64/.settings/.api_filters b/binaries/org.eclipse.swt.win32.win32.x86_64/.settings/.api_filters index 53c577d6a5f..8344f670108 100644 --- a/binaries/org.eclipse.swt.win32.win32.x86_64/.settings/.api_filters +++ b/binaries/org.eclipse.swt.win32.win32.x86_64/.settings/.api_filters @@ -148,6 +148,20 @@ + + + + + + + + + + + + + + @@ -336,6 +350,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -353,11 +421,11 @@ - - + + - - + + diff --git a/bundles/org.eclipse.swt.skia/.classpath b/bundles/org.eclipse.swt.skia/.classpath new file mode 100644 index 00000000000..fdc7a41b2a9 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/.classpath @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.eclipse.swt.skia/.gitignore b/bundles/org.eclipse.swt.skia/.gitignore new file mode 100644 index 00000000000..3930f2c9de3 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/.gitignore @@ -0,0 +1,2 @@ +# Ignore all JARs and content in lib/ +lib/ diff --git a/bundles/org.eclipse.swt.skia/.project b/bundles/org.eclipse.swt.skia/.project new file mode 100644 index 00000000000..19123f5db20 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/.project @@ -0,0 +1,34 @@ + + + org.eclipse.swt.skia + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.pde.PluginNature + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + \ No newline at end of file diff --git a/bundles/org.eclipse.swt.skia/.settings/org.eclipse.core.resources.prefs b/bundles/org.eclipse.swt.skia/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000000..99f26c0203a --- /dev/null +++ b/bundles/org.eclipse.swt.skia/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/bundles/org.eclipse.swt.skia/.settings/org.eclipse.jdt.core.prefs b/bundles/org.eclipse.swt.skia/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000000..61ae3820f10 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,118 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.builder.annotationPath.allLocations=disabled +org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnull.secondary= +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault.secondary= +org.eclipse.jdt.core.compiler.annotation.notowning=org.eclipse.jdt.annotation.NotOwning +org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullable.secondary= +org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled +org.eclipse.jdt.core.compiler.annotation.owning=org.eclipse.jdt.annotation.Owning +org.eclipse.jdt.core.compiler.annotation.resourceanalysis=disabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=21 +org.eclipse.jdt.core.compiler.compliance=21 +org.eclipse.jdt.core.compiler.problem.APILeak=warning +org.eclipse.jdt.core.compiler.problem.annotatedTypeArgumentToUnannotated=info +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=warning +org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompatibleOwningContract=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.insufficientResourceAnalysis=warning +org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=warning +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=info +org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning +org.eclipse.jdt.core.compiler.problem.nonnullTypeVariableFromLegacyInvocation=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.pessimisticNullAnalysisForFreeTypeVariables=warning +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore +org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.suppressWarningsNotFullyAnalysed=info +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.terminalDeprecation=warning +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=warning +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentType=warning +org.eclipse.jdt.core.compiler.problem.unlikelyCollectionMethodArgumentTypeStrict=disabled +org.eclipse.jdt.core.compiler.problem.unlikelyEqualsArgumentType=info +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unstableAutoModuleName=warning +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLambdaParameter=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=21 diff --git a/bundles/org.eclipse.swt.skia/.settings/org.eclipse.jdt.ui.prefs b/bundles/org.eclipse.swt.skia/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 00000000000..cd093bf0eb7 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,152 @@ +eclipse.preferences.version=1 +editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true +org.eclipse.jdt.ui.text.custom_code_templates= +sp_cleanup.add_all=false +sp_cleanup.add_default_serial_version_id=true +sp_cleanup.add_generated_serial_version_id=false +sp_cleanup.add_missing_annotations=true +sp_cleanup.add_missing_deprecated_annotations=true +sp_cleanup.add_missing_methods=false +sp_cleanup.add_missing_nls_tags=false +sp_cleanup.add_missing_override_annotations=true +sp_cleanup.add_missing_override_annotations_interface_methods=true +sp_cleanup.add_serial_version_id=false +sp_cleanup.also_simplify_lambda=true +sp_cleanup.always_use_blocks=true +sp_cleanup.always_use_parentheses_in_expressions=false +sp_cleanup.always_use_this_for_non_static_field_access=false +sp_cleanup.always_use_this_for_non_static_method_access=false +sp_cleanup.array_with_curly=false +sp_cleanup.arrays_fill=false +sp_cleanup.bitwise_conditional_expression=false +sp_cleanup.boolean_literal=false +sp_cleanup.boolean_value_rather_than_comparison=false +sp_cleanup.break_loop=false +sp_cleanup.collection_cloning=false +sp_cleanup.comparing_on_criteria=false +sp_cleanup.comparison_statement=false +sp_cleanup.controlflow_merge=false +sp_cleanup.convert_functional_interfaces=true +sp_cleanup.convert_to_enhanced_for_loop=true +sp_cleanup.convert_to_enhanced_for_loop_if_loop_var_used=false +sp_cleanup.convert_to_switch_expressions=true +sp_cleanup.correct_indentation=true +sp_cleanup.do_while_rather_than_while=false +sp_cleanup.double_negation=false +sp_cleanup.else_if=false +sp_cleanup.embedded_if=false +sp_cleanup.evaluate_nullable=false +sp_cleanup.extract_increment=false +sp_cleanup.format_source_code=false +sp_cleanup.format_source_code_changes_only=false +sp_cleanup.hash=false +sp_cleanup.if_condition=false +sp_cleanup.insert_inferred_type_arguments=false +sp_cleanup.instanceof=false +sp_cleanup.instanceof_keyword=false +sp_cleanup.invert_equals=false +sp_cleanup.join=false +sp_cleanup.lazy_logical_operator=false +sp_cleanup.make_local_variable_final=true +sp_cleanup.make_parameters_final=false +sp_cleanup.make_private_fields_final=true +sp_cleanup.make_type_abstract_if_missing_method=false +sp_cleanup.make_variable_declarations_final=true +sp_cleanup.map_cloning=false +sp_cleanup.merge_conditional_blocks=false +sp_cleanup.multi_catch=false +sp_cleanup.never_use_blocks=false +sp_cleanup.never_use_parentheses_in_expressions=true +sp_cleanup.no_string_creation=false +sp_cleanup.no_super=false +sp_cleanup.number_suffix=false +sp_cleanup.objects_equals=false +sp_cleanup.on_save_use_additional_actions=true +sp_cleanup.one_if_rather_than_duplicate_blocks_that_fall_through=false +sp_cleanup.operand_factorization=false +sp_cleanup.organize_imports=true +sp_cleanup.overridden_assignment=false +sp_cleanup.overridden_assignment_move_decl=true +sp_cleanup.plain_replacement=false +sp_cleanup.precompile_regex=false +sp_cleanup.primitive_comparison=false +sp_cleanup.primitive_parsing=false +sp_cleanup.primitive_rather_than_wrapper=false +sp_cleanup.primitive_serialization=false +sp_cleanup.pull_out_if_from_if_else=false +sp_cleanup.pull_up_assignment=true +sp_cleanup.push_down_negation=false +sp_cleanup.qualify_static_field_accesses_with_declaring_class=false +sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +sp_cleanup.qualify_static_member_accesses_with_declaring_class=false +sp_cleanup.qualify_static_method_accesses_with_declaring_class=false +sp_cleanup.reduce_indentation=true +sp_cleanup.redundant_comparator=false +sp_cleanup.redundant_falling_through_block_end=false +sp_cleanup.remove_private_constructors=true +sp_cleanup.remove_redundant_modifiers=false +sp_cleanup.remove_redundant_semicolons=false +sp_cleanup.remove_redundant_type_arguments=false +sp_cleanup.remove_trailing_whitespaces=true +sp_cleanup.remove_trailing_whitespaces_all=true +sp_cleanup.remove_trailing_whitespaces_ignore_empty=false +sp_cleanup.remove_unnecessary_array_creation=false +sp_cleanup.remove_unnecessary_casts=true +sp_cleanup.remove_unnecessary_nls_tags=false +sp_cleanup.remove_unnecessary_suppress_warnings=false +sp_cleanup.remove_unused_imports=true +sp_cleanup.remove_unused_local_variables=false +sp_cleanup.remove_unused_method_parameters=false +sp_cleanup.remove_unused_private_fields=true +sp_cleanup.remove_unused_private_members=false +sp_cleanup.remove_unused_private_methods=true +sp_cleanup.remove_unused_private_types=true +sp_cleanup.replace_deprecated_calls=false +sp_cleanup.replace_deprecated_fields=false +sp_cleanup.return_expression=false +sp_cleanup.simplify_boolean_if_else=false +sp_cleanup.simplify_lambda_expression_and_method_ref=true +sp_cleanup.single_used_field=false +sp_cleanup.sort_members=false +sp_cleanup.sort_members_all=false +sp_cleanup.standard_comparison=false +sp_cleanup.static_inner_class=false +sp_cleanup.strictly_equal_or_different=false +sp_cleanup.stringbuffer_to_stringbuilder=false +sp_cleanup.stringbuilder=false +sp_cleanup.stringbuilder_for_local_vars=true +sp_cleanup.stringconcat_stringbuffer_stringbuilder=false +sp_cleanup.stringconcat_to_textblock=false +sp_cleanup.substring=false +sp_cleanup.switch=true +sp_cleanup.switch_for_instanceof_pattern=false +sp_cleanup.system_property=false +sp_cleanup.system_property_boolean=false +sp_cleanup.system_property_file_encoding=false +sp_cleanup.system_property_file_separator=false +sp_cleanup.system_property_javaspecversion=false +sp_cleanup.system_property_javaversion=false +sp_cleanup.system_property_line_separator=false +sp_cleanup.system_property_path_separator=false +sp_cleanup.ternary_operator=false +sp_cleanup.try_with_resource=false +sp_cleanup.unlooped_while=false +sp_cleanup.unreachable_block=false +sp_cleanup.use_anonymous_class_creation=false +sp_cleanup.use_autoboxing=false +sp_cleanup.use_blocks=true +sp_cleanup.use_blocks_only_for_return_and_throw=false +sp_cleanup.use_directly_map_method=false +sp_cleanup.use_lambda=true +sp_cleanup.use_parentheses_in_expressions=false +sp_cleanup.use_string_is_blank=false +sp_cleanup.use_this_for_non_static_field_access=false +sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true +sp_cleanup.use_this_for_non_static_method_access=false +sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true +sp_cleanup.use_unboxing=false +sp_cleanup.use_var=false +sp_cleanup.useless_continue=false +sp_cleanup.useless_return=false +sp_cleanup.valueof_rather_than_instantiation=false diff --git a/bundles/org.eclipse.swt.skia/META-INF/MANIFEST.MF b/bundles/org.eclipse.swt.skia/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..8f084b5bf5a --- /dev/null +++ b/bundles/org.eclipse.swt.skia/META-INF/MANIFEST.MF @@ -0,0 +1,23 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-SymbolicName: org.eclipse.swt.skia +Bundle-Version: 3.134.0.qualifier +Automatic-Module-Name: org.eclipse.swt.skia +Bundle-ClassPath: lib/skija-linux-x64-0.143.11.jar, + lib/skija-shared-0.143.11.jar, + lib/skija-windows-x64-0.143.11.jar, + lib/types-0.1.1.jar, + lib/, + ./ +Bundle-Name: %fragmentName +Bundle-Vendor: %providerName +Bundle-Localization: fragment +Bundle-RequiredExecutionEnvironment: JavaSE-21 +Eclipse-ExtensibleAPI: true +Fragment-Host: org.eclipse.swt;bundle-version="[3.134.0.qualifier,4.0.0)" +Require-Bundle: io.github.humbleui.skija-shared;bundle-version="0.143.13", + io.github.humbleui.types;bundle-version="0.2.0" +Export-Package: org.eclipse.swt.internal.canvasext;version="0.0.1";x-friends:="org.eclipse.swt.tests.skia", + org.eclipse.swt.internal.graphics;version="0.0.1";x-friends:="org.eclipse.swt.tests.skia", + org.eclipse.swt.internal.skia;version="0.0.1";x-friends:="org.eclipse.swt.tests.skia", + org.eclipse.swt.internal.skia.cache;version="0.0.1";x-friends:="org.eclipse.swt.tests.skia" diff --git a/bundles/org.eclipse.swt.skia/META-INF/p2.inf b/bundles/org.eclipse.swt.skia/META-INF/p2.inf new file mode 100644 index 00000000000..1404a49b59f --- /dev/null +++ b/bundles/org.eclipse.swt.skia/META-INF/p2.inf @@ -0,0 +1,40 @@ +# ensure that the applicable implementation fragment gets installed (https://github.com/eclipse-platform/eclipse.platform.releng.aggregator/issues/3006) +requires.1.namespace = org.eclipse.equinox.p2.iu +requires.1.name = org.eclipse.swt.win32.win32.x86_64 +requires.1.filter = (&(osgi.os=win32)(osgi.ws=win32)(osgi.arch=x86_64)) + +requires.2.namespace = org.eclipse.equinox.p2.iu +requires.2.name = org.eclipse.swt.cocoa.macosx.x86_64 +requires.2.filter = (&(osgi.os=macosx)(osgi.ws=cocoa)(osgi.arch=x86_64)) + +requires.3.namespace = org.eclipse.equinox.p2.iu +requires.3.name = org.eclipse.swt.gtk.linux.x86_64 +requires.3.filter = (&(osgi.os=linux)(osgi.ws=gtk)(osgi.arch=x86_64)) + +requires.4.namespace = org.eclipse.equinox.p2.iu +requires.4.name = org.eclipse.swt.gtk.linux.ppc64le +requires.4.filter = (&(osgi.os=linux)(osgi.ws=gtk)(osgi.arch=ppc64le)) + +requires.5.namespace = org.eclipse.equinox.p2.iu +requires.5.name = org.eclipse.swt.gtk.linux.aarch64 +requires.5.filter = (&(osgi.os=linux)(osgi.ws=gtk)(osgi.arch=aarch64)) + +requires.6.namespace = org.eclipse.equinox.p2.iu +requires.6.name = org.eclipse.swt.cocoa.macosx.aarch64 +requires.6.filter = (&(osgi.os=macosx)(osgi.ws=cocoa)(osgi.arch=aarch64)) + +requires.7.namespace = org.eclipse.equinox.p2.iu +requires.7.name = org.eclipse.swt.win32.win32.aarch64 +requires.7.filter = (&(osgi.os=win32)(osgi.ws=win32)(osgi.arch=aarch64)) + +requires.8.namespace = org.eclipse.equinox.p2.iu +requires.8.name = org.eclipse.swt.gtk.linux.riscv64 +requires.8.filter = (&(osgi.os=linux)(osgi.ws=gtk)(osgi.arch=riscv64)) + +requires.9.namespace = org.eclipse.equinox.p2.iu +requires.9.name = io.github.humbleui.skija-windows-x64 +requires.9.filter = (&(osgi.os=win32)(osgi.ws=win32)(osgi.arch=x86_64)) + +requires.10.namespace = org.eclipse.equinox.p2.iu +requires.10.name = io.github.humbleui.skija-linux-x64 +requires.10.filter = (&(osgi.os=linux)(osgi.ws=gtk)(osgi.arch=x86_64)) \ No newline at end of file diff --git a/bundles/org.eclipse.swt.skia/Readme.md b/bundles/org.eclipse.swt.skia/Readme.md new file mode 100644 index 00000000000..d1cd04a482e --- /dev/null +++ b/bundles/org.eclipse.swt.skia/Readme.md @@ -0,0 +1,36 @@ +# org.eclipse.swt.skia + +Skia Plugin for the SWT User Interface Library +============================================ + +This plugin integrates the Skia graphics library into SWT, enabling modern and high-performance 2D graphics rendering. + +## Building and Testing Locally + +### Prerequisites +- **Eclipse IDE** +- **M2E – Maven Integration for Eclipse** ([m2e](https://eclipse.dev/m2e/)) + +### Steps +1. Import the following projects into your Eclipse workspace: + - `org.eclipse.swt` + - `org.eclipse.swt.skia` + - The SWT fragment(s) for your platform (e.g., `org.eclipse.swt.gtk.linux.x86_64`) + +2. Build the projects using Maven: + - Right-click the `pom.xml` in `org.eclipse.swt` → `Run As` → `Maven install` + - Right-click the `pom.xml` in `org.eclipse.swt.skia` → `Run As` → `Maven generate-sources` + + **Note:** The second build may fail. If this happens, close the Skia project, reopen it, and run Maven install again on the Skia project. The build should then succeed. + +3. The Skija dependencies will be automatically downloaded into the `lib` directory of the Skia plugin. + +4. If you receive error messages stating that the Skija jar files cannot be found, please check that the jar files are present in the `lib` directory and that the native libraries inside are not empty. You can verify this with a text editor. + If the jar files are empty, you can delete them and execute `Maven generate-sources` again. This is a workaround because Tycho might not be able to download the dependencies yet. + +### Notes +- The Skia plugin is currently in an early development stage, and there may be issues with the build process. +- The skija source jars of types and shared also will be downloaded to the `lib` directory. You can add them to your classpath, if you wish to check the source code of Skija. +--- + +For more information, please refer to the project documentation or contact the maintainers. \ No newline at end of file diff --git a/bundles/org.eclipse.swt.skia/SWT Skia Tests/org/eclipse/swt/tests/skia/SupportedTestPlatforms.java b/bundles/org.eclipse.swt.skia/SWT Skia Tests/org/eclipse/swt/tests/skia/SupportedTestPlatforms.java new file mode 100644 index 00000000000..0f74aaa27a4 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/SWT Skia Tests/org/eclipse/swt/tests/skia/SupportedTestPlatforms.java @@ -0,0 +1,28 @@ +package org.eclipse.swt.tests.skia; + +import org.eclipse.swt.SWT; + +public class SupportedTestPlatforms { + + public static boolean isSupported() { + return isFittingOS() && isFittingArchitecture(); + } + + public static boolean isFittingOS() { + return "win32".equals(SWT.getPlatform()) || "gtk".equals(SWT.getPlatform()); + } + + public static boolean isFittingArchitecture() { + final var arc = arch(); + return "x86_64".equals(arc); + } + + static String arch() { + final String osArch = System.getProperty("os.arch"); //$NON-NLS-1$ + if (osArch.equals ("amd64")) { //$NON-NLS-1$ + return "x86_64"; + } + return osArch; + } + +} diff --git a/bundles/org.eclipse.swt.skia/SWT Skia Tests/org/eclipse/swt/tests/skia/Test_org_eclipse_swt_skia_RGBAEncoder.java b/bundles/org.eclipse.swt.skia/SWT Skia Tests/org/eclipse/swt/tests/skia/Test_org_eclipse_swt_skia_RGBAEncoder.java new file mode 100644 index 00000000000..5a83dd94d1e --- /dev/null +++ b/bundles/org.eclipse.swt.skia/SWT Skia Tests/org/eclipse/swt/tests/skia/Test_org_eclipse_swt_skia_RGBAEncoder.java @@ -0,0 +1,194 @@ +package org.eclipse.swt.tests.skia; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.PaletteData; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.internal.graphics.RGBAEncoder; +import org.junit.jupiter.api.Test; + +public class Test_org_eclipse_swt_skia_RGBAEncoder { + + @Test + public void testIndexed8BitNoTransparency() { + // Palette with red, green, blue + final PaletteData palette = new PaletteData(new RGB(255,0,0), new RGB(0,255,0), new RGB(0,0,255)); + final byte[] data = {0, 1, 2, 0}; // 2x2: R G + // B R + final ImageData img = new ImageData(2, 2, 8, palette, 1, data); + final byte[] rgba = RGBAEncoder.encode(img); + // Expect RGBA for each pixel + assertArrayEquals(new byte[] { + (byte)255,0,0,(byte)255, 0,(byte)255,0,(byte)255, + 0,0,(byte)255,(byte)255, (byte)255,0,0,(byte)255 + }, rgba); + } + + @Test + public void testIndexed8BitTransparentPixel() { + // Palette with two colors, one transparent + final PaletteData palette = new PaletteData(new RGB(1,2,3), new RGB(4,5,6)); + final byte[] data = {0, 1, 0, 1}; + final ImageData img = new ImageData(2, 2, 8, palette, 1, data); + img.transparentPixel = 1; + final byte[] rgba = RGBAEncoder.encode(img); + // Transparent pixel should have alpha 0 + assertEquals(0, rgba[7]); // pixel (0,1) transparent + assertEquals(0, rgba[15]); // pixel (1,1) transparent + assertEquals((byte)255, rgba[3]); // pixel (0,0) opaque + } + + @Test + public void testIndexed8BitAlphaData() { + // Palette with two colors, alphaData set + final PaletteData palette = new PaletteData(new RGB(10,20,30), new RGB(40,50,60)); + final byte[] data = {0, 1, 0, 1}; + final ImageData img = new ImageData(2, 2, 8, palette, 1, data); + img.alphaData = new byte[] {0, (byte)128, (byte)255, (byte)64}; + final byte[] rgba = RGBAEncoder.encode(img); + // Check alpha values for each pixel + assertEquals(0, rgba[3]); // pixel (0,0) + assertEquals((byte)128, rgba[7]); // pixel (1,0) + assertEquals((byte)255, rgba[11]); // pixel (0,1) + assertEquals((byte)64, rgba[15]); // pixel (1,1) + } + + @Test + public void testIndexed8BitMaskData() { + // Palette with two colors, maskData set + final PaletteData palette = new PaletteData(new RGB(1,2,3), new RGB(4,5,6)); + final byte[] data = {0, 1, 0, 1}; + final ImageData img = new ImageData(2, 2, 8, palette, 1, data); + img.maskData = new byte[] {(byte)0b10100000}; // Only (0,0) and (0,1) visible + final byte[] rgba = RGBAEncoder.encode(img); + // Check alpha for mask + assertEquals((byte)255, rgba[3]); // (0,0) visible + assertEquals(0, rgba[7]); // (1,0) masked + assertEquals((byte)255, rgba[11]);// (0,1) visible + assertEquals(0, rgba[15]); // (1,1) masked + } + + @Test + public void testDirect24BitNoTransparency() { + // Direct palette, 24-bit, no transparency + final PaletteData palette = new PaletteData(0xFF0000, 0x00FF00, 0x0000FF); + final byte[] data = new byte[4*4]; + final ImageData img = new ImageData(2, 2, 24, palette, 1, data); + img.setPixel(0,0, 0x112233); + img.setPixel(1,0, 0x445566); + img.setPixel(0,1, 0x778899); + img.setPixel(1,1, 0xAABBCC); + final byte[] rgba = RGBAEncoder.encode(img); + // Check RGBA values for each pixel + assertEquals(0x11, rgba[0] & 0xFF); assertEquals(0x22, rgba[1] & 0xFF); assertEquals(0x33, rgba[2] & 0xFF); + assertEquals(255, rgba[3] & 0xFF); + assertEquals(0x44, rgba[4] & 0xFF); assertEquals(0x55, rgba[5] & 0xFF); assertEquals(0x66, rgba[6] & 0xFF); + assertEquals(255, rgba[7] & 0xFF); + assertEquals(0x77, rgba[8] & 0xFF); assertEquals(0x88, rgba[9] & 0xFF); assertEquals(0x99, rgba[10] & 0xFF); + assertEquals(255, rgba[11] & 0xFF); + assertEquals(0xAA, rgba[12] & 0xFF); assertEquals(0xBB, rgba[13] & 0xFF); assertEquals(0xCC, rgba[14] & 0xFF); + assertEquals(255, rgba[15] & 0xFF); + } + + @Test + public void testDirect24BitAlphaData() { + // Direct palette, 24-bit, with alphaData + final PaletteData palette = new PaletteData(0xFF0000, 0x00FF00, 0x0000FF); + final byte[] data = new byte[4*4]; + final ImageData img = new ImageData(2, 2, 24, palette, 1, data); + img.setPixel(0,0, 0x123456); + img.setPixel(1,0, 0x654321); + img.setPixel(0,1, 0xABCDEF); + img.setPixel(1,1, 0x0F0F0F); + img.alphaData = new byte[] {0, (byte)128, (byte)255, (byte)64}; + final byte[] rgba = RGBAEncoder.encode(img); + // Check alpha values + assertEquals(0, rgba[3]); + assertEquals((byte)128, rgba[7]); + assertEquals((byte)255, rgba[11]); + assertEquals((byte)64, rgba[15]); + } + + @Test + public void testDirect24BitMaskData() { + // Direct palette, 24-bit, with maskData + final PaletteData palette = new PaletteData(0xFF0000, 0x00FF00, 0x0000FF); + final byte[] data = new byte[4*4]; + final ImageData img = new ImageData(2, 2, 24, palette, 1, data); + img.setPixel(0,0, 0x123456); + img.setPixel(1,0, 0x654321); + img.setPixel(0,1, 0xABCDEF); + img.setPixel(1,1, 0x0F0F0F); + img.maskData = new byte[] {(byte)0b10100000}; + final byte[] rgba = RGBAEncoder.encode(img); + // Check alpha for mask + assertEquals((byte)255, rgba[3]); // (0,0) visible + assertEquals(0, rgba[7]); // (1,0) masked + assertEquals((byte)255, rgba[11]);// (0,1) visible + assertEquals(0, rgba[15]); // (1,1) masked + } + + @Test + public void testDirect24BitGlobalAlpha() { + // Direct palette, 24-bit, with global alpha + final PaletteData palette = new PaletteData(0xFF0000, 0x00FF00, 0x0000FF); + final byte[] data = new byte[4*4]; + final ImageData img = new ImageData(2, 2, 24, palette, 1, data); + img.setPixel(0,0, 0x123456); + img.setPixel(1,0, 0x654321); + img.setPixel(0,1, 0xABCDEF); + img.setPixel(1,1, 0x0F0F0F); + img.alpha = 42; + final byte[] rgba = RGBAEncoder.encode(img); + // All alpha values should be 42 + for (int i = 3; i < rgba.length; i += 4) { + assertEquals((byte)42, rgba[i]); + } + } + + @Test + public void testIndexed1BitNoTransparency() { + // 1-bit indexed palette, no transparency + final PaletteData palette = new PaletteData(new RGB(0,0,0), new RGB(255,255,255)); + final byte[] data = {(byte)0b00000000, (byte)0b01000000}; // Row 1: 0 0, Row 2: 0 1 + final ImageData img = new ImageData(2, 2, 1, palette, 1, data); + final byte[] rgba = RGBAEncoder.encode(img); + // Check RGBA values for each pixel + assertEquals((byte)0, rgba[0]); assertEquals((byte)0, rgba[1]); assertEquals((byte)0, rgba[2]); + assertEquals((byte)255, rgba[3]); + assertEquals((byte)0, rgba[4]); assertEquals((byte)0, rgba[5]); assertEquals((byte)0, rgba[6]); + assertEquals((byte)255, rgba[7]); + assertEquals((byte)0, rgba[8]); assertEquals((byte)0, rgba[9]); assertEquals((byte)0, rgba[10]); + assertEquals((byte)255, rgba[11]); + assertEquals((byte)255, rgba[12]); assertEquals((byte)255, rgba[13]); assertEquals((byte)255, rgba[14]); + assertEquals((byte)255, rgba[15]); + } + + @Test + public void testIndexed4BitNoTransparency() { + // 4-bit indexed palette, no transparency + final PaletteData palette = new PaletteData( + new RGB(10,20,30), new RGB(40,50,60), new RGB(70,80,90), new RGB(100,110,120), + new RGB(130,140,150), new RGB(160,170,180), new RGB(190,200,210), new RGB(220,230,240), + new RGB(250,251,252), new RGB(1,2,3), new RGB(4,5,6), new RGB(7,8,9), + new RGB(11,12,13), new RGB(14,15,16), new RGB(17,18,19), new RGB(20,21,22) + ); + final byte[] data = {(byte)0x12, (byte)0x34}; // 2x2: 1 2 + // 3 4 + final ImageData img = new ImageData(2, 2, 4, palette, 1, data); + img.setPixel(0,0, 1); img.setPixel(1,0, 2); img.setPixel(0,1, 3); img.setPixel(1,1, 4); + final byte[] rgba = RGBAEncoder.encode(img); + // Check RGBA values for each pixel + assertEquals((byte)40, rgba[0]); assertEquals((byte)50, rgba[1]); assertEquals((byte)60, rgba[2]); + assertEquals((byte)255, rgba[3]); + assertEquals((byte)70, rgba[4]); assertEquals((byte)80, rgba[5]); assertEquals((byte)90, rgba[6]); + assertEquals((byte)255, rgba[7]); + assertEquals((byte)100, rgba[8]); assertEquals((byte)110, rgba[9]); assertEquals((byte)120, rgba[10]); + assertEquals((byte)255, rgba[11]); + assertEquals((byte)130, rgba[12]); assertEquals((byte)140, rgba[13]); assertEquals((byte)150, rgba[14]); + assertEquals((byte)255, rgba[15]); + } + +} \ No newline at end of file diff --git a/bundles/org.eclipse.swt.skia/SWT Skia Tests/org/eclipse/swt/tests/skia/Test_org_eclipse_swt_skia_RectangleConverter.java b/bundles/org.eclipse.swt.skia/SWT Skia Tests/org/eclipse/swt/tests/skia/Test_org_eclipse_swt_skia_RectangleConverter.java new file mode 100644 index 00000000000..6320e86364d --- /dev/null +++ b/bundles/org.eclipse.swt.skia/SWT Skia Tests/org/eclipse/swt/tests/skia/Test_org_eclipse_swt_skia_RectangleConverter.java @@ -0,0 +1,101 @@ +package org.eclipse.swt.tests.skia; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.internal.graphics.RectangleConverter; +import org.eclipse.swt.internal.skia.DpiScalerUtil; +import org.junit.jupiter.api.Test; + +import io.github.humbleui.types.RRect; +import io.github.humbleui.types.Rect; + +public class Test_org_eclipse_swt_skia_RectangleConverter { + private final DpiScalerUtil scaler100 = new DpiScalerUtil(100); + private final DpiScalerUtil scaler150 = new DpiScalerUtil(150); + + @Test + void testCreateScaledRectangle_Rectangle() { + final Rectangle r = new Rectangle(10, 20, 30, 40); + final Rect rect = RectangleConverter.createScaledRectangle(scaler100, r); + assertEquals(10, rect.getLeft()); + assertEquals(20, rect.getTop()); + assertEquals(40, rect.getRight()); + assertEquals(60, rect.getBottom()); + } + + @Test + void testScaleUpRectangle() { + final Rectangle r = new Rectangle(10, 20, 30, 40); + final Rectangle scaled = RectangleConverter.scaleUpRectangle(scaler150, r); + assertEquals(15, scaled.x); + assertEquals(30, scaled.y); + assertEquals(45, scaled.width); + assertEquals(60, scaled.height); + } + + @Test + void testCreateScaledRectangle_Ints() { + final Rect rect = RectangleConverter.createScaledRectangle(scaler150, 10, 20, 30, 40); + assertEquals(15, rect.getLeft()); + assertEquals(30, rect.getTop()); + assertEquals(60, rect.getRight()); + assertEquals(90, rect.getBottom()); + } + + @Test + void testGetScaledOffsetValue() { + assertEquals(0.5f, RectangleConverter.getScaledOffsetValue(scaler100, 0)); + assertEquals(0.5f, RectangleConverter.getScaledOffsetValue(scaler100, 1)); + assertEquals(0f, RectangleConverter.getScaledOffsetValue(scaler100, 2)); + assertEquals(0f, RectangleConverter.getScaledOffsetValue(scaler150, 1)); + assertEquals(0.75f, RectangleConverter.getScaledOffsetValue(scaler150, 2)); + } + + @Test + void testOffsetRectangle_Rect() { + final Rect rect = new Rect(1, 2, 11, 22); + final Rect off = RectangleConverter.offsetRectangle(scaler100, 1, rect); + assertEquals(1.5f, off.getLeft()); + assertEquals(2.5f, off.getTop()); + assertEquals(11.5f, off.getRight()); + assertEquals(22.5f, off.getBottom()); + // No offset for even lineWidth + final Rect off2 = RectangleConverter.offsetRectangle(scaler100, 2, rect); + assertEquals(rect, off2); + } + + @Test + void testOffsetRectangle_RRect() { + final RRect rrect = new RRect(1, 2, 11, 22, new float[] { 3, 4 }); + final RRect off = RectangleConverter.offsetRectangle(scaler100, 1, rrect); + assertEquals(1.5f, off.getLeft()); + assertEquals(2.5f, off.getTop()); + assertEquals(11.5f, off.getRight()); + assertEquals(22.5f, off.getBottom()); + assertArrayEquals(new float[] { 3, 4 }, off._radii); + // No offset for even lineWidth + final RRect off2 = RectangleConverter.offsetRectangle(scaler100, 2, rrect); + assertEquals(rrect, off2); + } + + @Test + void testCreateScaledRectangleWithOffset() { + final Rect rect = RectangleConverter.createScaledRectangleWithOffset(scaler100, 1, 10, 20, 30, 40); + assertEquals(10.5f, rect.getLeft()); + assertEquals(20.5f, rect.getTop()); + assertEquals(40.5f, rect.getRight()); + assertEquals(60.5f, rect.getBottom()); + } + + @Test + void testCreateScaledRoundRectangle() { + final RRect rrect = RectangleConverter.createScaledRoundRectangle(scaler150, 10, 20, 30, 40, 5, 6); + assertEquals(15, rrect.getLeft()); + assertEquals(30, rrect.getTop()); + assertEquals(60, rrect.getRight()); + assertEquals(90, rrect.getBottom()); + assertArrayEquals(new float[] { 7.5f, 9f }, rrect._radii); + } +} \ No newline at end of file diff --git a/bundles/org.eclipse.swt.skia/SWT Skia Tests/org/eclipse/swt/tests/skia/Test_org_eclipse_swt_skia_SkijaToSwtImageConvert.java b/bundles/org.eclipse.swt.skia/SWT Skia Tests/org/eclipse/swt/tests/skia/Test_org_eclipse_swt_skia_SkijaToSwtImageConvert.java new file mode 100644 index 00000000000..543adbac31e --- /dev/null +++ b/bundles/org.eclipse.swt.skia/SWT Skia Tests/org/eclipse/swt/tests/skia/Test_org_eclipse_swt_skia_SkijaToSwtImageConvert.java @@ -0,0 +1,40 @@ +package org.eclipse.swt.tests.skia; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.internal.graphics.SkijaToSwtImageConverter; +import org.junit.jupiter.api.Test; + +import io.github.humbleui.skija.ColorAlphaType; +import io.github.humbleui.skija.ColorType; +import io.github.humbleui.skija.Image; +import io.github.humbleui.skija.ImageInfo; +import io.github.humbleui.skija.Surface; + +class Test_org_eclipse_swt_skia_SkijaToSwtImageConvert { + + @Test + public void testConvertSimpleRGBAImage() { + // Create a 2x2 Skija image with RGBA_8888 + try (Surface surface = Surface.makeRaster(new ImageInfo(2, 2, ColorType.RGBA_8888, ColorAlphaType.PREMUL))) { + surface.getCanvas().clear(0xFF0000FF); // Blue + final Image skijaImage = surface.makeImageSnapshot(); + final ImageData data = SkijaToSwtImageConverter.convertSkijaImageToImageData(skijaImage); + assertNotNull(data); + assertEquals(2, data.width); + assertEquals(2, data.height); + // Check if the data is blue (0,0,255) + final int r = data.data[0] & 0xFF; + final int g = data.data[1] & 0xFF; + final int b = data.data[2] & 0xFF; + // Print actual values for debugging + System.out.printf("Pixel (0,0): R=%d G=%d B=%d\n", r, g, b); + assertEquals(0, r, "Red channel should be 0"); + assertEquals(0, g, "Green channel should be 0"); + assertEquals(255, b, "Blue channel should be 255"); + } + } + +} diff --git a/bundles/org.eclipse.swt.skia/SWT Skia Tests/org/eclipse/swt/tests/skia/Test_org_eclipse_swt_skia_SkijaToSwtImageConverter.java b/bundles/org.eclipse.swt.skia/SWT Skia Tests/org/eclipse/swt/tests/skia/Test_org_eclipse_swt_skia_SkijaToSwtImageConverter.java new file mode 100644 index 00000000000..18d2d8b5077 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/SWT Skia Tests/org/eclipse/swt/tests/skia/Test_org_eclipse_swt_skia_SkijaToSwtImageConverter.java @@ -0,0 +1,69 @@ +package org.eclipse.swt.tests.skia; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.internal.graphics.SkijaToSwtImageConverter; +import org.junit.jupiter.api.Test; + +import io.github.humbleui.skija.ColorAlphaType; +import io.github.humbleui.skija.ColorType; +import io.github.humbleui.skija.Image; +import io.github.humbleui.skija.ImageInfo; +import io.github.humbleui.skija.Surface; + +public class Test_org_eclipse_swt_skia_SkijaToSwtImageConverter { + + @Test + public void testConvertSimpleRGBAImage() { + // Create a 2x2 Skija image with RGBA_8888 + try (Surface surface = Surface.makeRaster(new ImageInfo(2, 2, ColorType.RGBA_8888, ColorAlphaType.PREMUL))) { + surface.getCanvas().clear(0xFF0000FF); // Blue + final Image skijaImage = surface.makeImageSnapshot(); + final ImageData data = SkijaToSwtImageConverter.convertSkijaImageToImageData(skijaImage); + assertNotNull(data); + assertEquals(2, data.width); + assertEquals(2, data.height); + // Check if the data is blue (0,0,255) + final int r = data.data[0] & 0xFF; + final int g = data.data[1] & 0xFF; + final int b = data.data[2] & 0xFF; + // Print actual values for debugging + System.out.printf("Pixel (0,0): R=%d G=%d B=%d\n", r, g, b); + assertEquals(0, r, "Red channel should be 0"); + assertEquals(0, g, "Green channel should be 0"); + assertEquals(255, b, "Blue channel should be 255"); + } + } + + @Test + public void testConvertAlphaHandling() { + // Create a 1x1 Skija image with alpha + try (Surface surface = Surface.makeRaster(new ImageInfo(1, 1, ColorType.RGBA_8888, ColorAlphaType.PREMUL))) { + surface.getCanvas().clear(0x80000000); // Half-transparent black + final Image skijaImage = surface.makeImageSnapshot(); + final ImageData data = SkijaToSwtImageConverter.convertSkijaImageToImageData(skijaImage); + assertNotNull(data); + assertEquals(1, data.width); + assertEquals(1, data.height); + // Print actual alpha value for debugging + final int alpha = data.alphaData[0] & 0xFF; + System.out.printf("Pixel (0,0) alpha: %d\n", alpha); + // Check if alpha is between 1 and 254 (exclusive) + assertTrue(alpha > 0 && alpha < 255, "Alpha should be between 1 and 254, but was: " + alpha); + } + } + + @Test + public void testNullInput() { + // Should throw AssertionError or NullPointerException on null input + final Throwable thrown = assertThrows(Throwable.class, () -> { + SkijaToSwtImageConverter.convertSkijaImageToImageData(null); + }); + assertTrue(thrown instanceof NullPointerException || thrown instanceof AssertionError, + "Expected NullPointerException or AssertionError, but got: " + thrown.getClass().getName()); + } +} \ No newline at end of file diff --git a/bundles/org.eclipse.swt.skia/about.html b/bundles/org.eclipse.swt.skia/about.html new file mode 100644 index 00000000000..2a38755d1cf --- /dev/null +++ b/bundles/org.eclipse.swt.skia/about.html @@ -0,0 +1,19 @@ + + + + +About + + +

About This Content

+ +

Date...

+

License

+ +

+TODO: license for Skija dependencies?? +

+ + + \ No newline at end of file diff --git a/bundles/org.eclipse.swt.skia/build.properties b/bundles/org.eclipse.swt.skia/build.properties new file mode 100644 index 00000000000..17cc19f5fee --- /dev/null +++ b/bundles/org.eclipse.swt.skia/build.properties @@ -0,0 +1,32 @@ +############################################################################### +# Copyright (c) 2025, 2025 SAP SE GmbH and others. +# +# This program and the accompanying materials +# are made available under the terms of the Eclipse Public License 2.0 +# which accompanies this distribution, and is available at +# https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +# +# Contributors: +# Denis Ungemach (SAP SE) - initial API and implementation +############################################################################### +source.. = \ + src/,\ + resources/ +output.. = bin/ +bin.includes = META-INF/,\ + fragment.properties,\ + about.html,\ + . +src.includes = about.html\ + +jars.extra.classpath = platform:/plugin/org.eclipse.swt.cocoa.macosx.aarch64,\ + platform:/plugin/org.eclipse.swt.cocoa.macosx.x86_64,\ + platform:/plugin/org.eclipse.swt.gtk.linux.aarch64,\ + platform:/plugin/org.eclipse.swt.gtk.linux.ppc64le,\ + platform:/plugin/org.eclipse.swt.gtk.linux.riscv64,\ + platform:/plugin/org.eclipse.swt.gtk.linux.x86_64,\ + platform:/plugin/org.eclipse.swt.win32.win32.aarch64,\ + platform:/plugin/org.eclipse.swt.win32.win32.x86_64 +custom = true diff --git a/bundles/org.eclipse.swt.skia/fragment.properties b/bundles/org.eclipse.swt.skia/fragment.properties new file mode 100644 index 00000000000..28f10d19a52 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/fragment.properties @@ -0,0 +1,14 @@ +############################################################################### +# Copyright (c) 2025 SAP SE and others. +# +# This program and the accompanying materials are made available under the terms of the Eclipse +# Public License 2.0 which accompanies this distribution, and is available at +# https://www.eclipse.org/legal/epl-2.0/ +# +# SPDX-License-Identifier: EPL-2.0 +# +# Contributors: +# Denis Ungemach (SAP SE) - initial API and implementation +############################################################################### +fragmentName = SWT Skia Support +providerName = Eclipse.org diff --git a/bundles/org.eclipse.swt.skia/pom.xml b/bundles/org.eclipse.swt.skia/pom.xml new file mode 100644 index 00000000000..46f864c81c6 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/pom.xml @@ -0,0 +1,94 @@ + + 4.0.0 + + org.eclipse.platform + eclipse.platform.swt + 4.40.0-SNAPSHOT + ../../ + + org.eclipse.swt.skia + 3.134.0-SNAPSHOT + eclipse-plugin + + + false + + + + + orbit-simrel-milestone + https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/milestone/S202604250819 + p2 + + + + + + + macos-skip-tests + + + mac + + + + true + + + + + + + + org.eclipse.tycho + target-platform-configuration + + consider + + ignore + + true + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + + io.github.humbleui.skija.* + false + + + + + org.apache.maven.plugins + maven-surefire-plugin + + ${doSkipTests} + + + + org.apache.maven.surefire + surefire-junit-platform + ${surefire.version} + + + + + execute-tests + + test + + + + + + + + + \ No newline at end of file diff --git a/bundles/org.eclipse.swt.skia/resources/META-INF/services/org.eclipse.swt.internal.canvasext.IExternalCanvasFactory b/bundles/org.eclipse.swt.skia/resources/META-INF/services/org.eclipse.swt.internal.canvasext.IExternalCanvasFactory new file mode 100644 index 00000000000..f3a76839c47 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/resources/META-INF/services/org.eclipse.swt.internal.canvasext.IExternalCanvasFactory @@ -0,0 +1 @@ +org.eclipse.swt.internal.canvasext.SkiaCanvasFactory \ No newline at end of file diff --git a/bundles/org.eclipse.swt.skia/skia.md b/bundles/org.eclipse.swt.skia/skia.md new file mode 100644 index 00000000000..4f53a83312f --- /dev/null +++ b/bundles/org.eclipse.swt.skia/skia.md @@ -0,0 +1,505 @@ +# Architecture: Skia Canvas Feature (`org.eclipse.swt.skia`) + +## Overview + +The `org.eclipse.swt.skia` plugin extends SWT with hardware-accelerated 2D rendering using the +[Skija](https://github.com/HumbleUI/Skija) Java bindings for the [Skia](https://skia.org/) graphics +library. It integrates transparently into the existing SWT drawing lifecycle and provides with SWT.SKIA a switch between the native rendering backend for canvases and the Skia drawing framework. + +This feature is currently only supported for windows and linux and it requires OpenGL support. + +The plugin org.eclipse.swt.skia is structured into four Java packages: + +| Package | Purpose | +|---|---| +| `org.eclipse.swt.internal.skia` | Core extension classes: rendering pipeline, resource management, DPI scaling utility (`DpiScalerUtil`) | +| `org.eclipse.swt.internal.skia.cache` | Cache key records for image, text-image, and text-split caches (`ImageKey`, `ImageTextKey`, `SplitsTextCache`) | +| `org.eclipse.swt.internal.graphics` | GC implementation (`SkiaGC`), paint management (`SkiaPaintManager`), text drawing (`SkiaTextDrawing`), image conversion (`SwtToSkiaImageConverter`, `SkijaToSwtImageConverter`, `RGBAEncoder`), color conversion (`SkiaColorConverter`), path conversion (`SkiaPathConverter`), transform conversion (`SkiaTransformConverter`), region calculation (`SkiaRegionCalculator`), rectangle/coordinate conversion (`RectangleConverter`), font metrics (`SkiaFontMetrics`) | +| `org.eclipse.swt.internal.canvasext` | Factory class (`SkiaCanvasFactory`) and logging utility (`Logger`) that plug the Skia backend into the SWT canvas extension framework | + +--- + +## Modification in the existing binary plugins of SWT + +To support Skia drawing, only minimal changes were made to the SWT binary fragments. Importantly, these fragments do not depend directly on any Skia resources. Instead, they introduce a lightweight, independent extension mechanism: painting commands are delegated to external handlers (such as the Skia fragment) using the Java ServiceLoader (see `ExternalCanvasHandler`). + +If the Skia fragment is not present, SWT automatically falls back to the classic rendering path. When a `Canvas` is instantiated with the `SWT.SKIA` style, the `externalCanvasHandler` field is initialized (if available), and all paint method calls are routed through this handler. + +To enable a GC API for Skia, the `final` modifier on `GC` was replaced with `sealed`, allowing only the new `GCExtension` subclass (added in the SWT fragments). `GCExtension` simply delegates all `GC` method calls to an internal delegate. The same approach is used for `FontMetrics` with `FontMetricsExtension`. + +In order to use Skia, a connection with OpenGL must be established. For this the `GLCanvasExtension` was created, which is a duplication of the `GLCanvas`, just without extending the class hierarchy of `Canvas`. In order to send a Paint event with OpenGL, the `GLPaintEventInvoker` provides the necessary delegation mechanism. + +There are also other small modifications to enable Skia drawing. But these are no modifications to the SWT API. + +The following packages were added or modified in the SWT binary fragments: + +| Package | Purpose | +|---|---| +| `org.eclipse.swt.internal.canvasext` | Extension framework: `ExternalCanvasHandler`, `IExternalCanvasFactory`, `IExternalCanvasHandler`, `IExternalGC`, `IExternalFontMetrics`, `DpiScaler`, `FontProperties` | +| `org.eclipse.swt.opengl` | OpenGL integration: `GLCanvasExtension`, `GLPaintEventInvoker` | +| `org.eclipse.swt.graphics` | Extension subclasses: `GCExtension`, `FontMetricsExtension` | + +--- + +### Skia Handler Implementation + +The Skia backend provides its handler via the `SkiaCanvasFactory` class, which implements `IExternalCanvasFactory` and is registered as a service in the OSGi bundle via: + +``` +resources/META-INF/services/org.eclipse.swt.internal.canvasext.IExternalCanvasFactory +``` + +with the content: + +``` +org.eclipse.swt.internal.canvasext.SkiaCanvasFactory +``` + +This enables the `ServiceLoader` to discover the Skia factory at runtime. + +### Handler Lifecycle and Resource Management + +The handler instance (e.g., `SkiaGlCanvasExtension`) is responsible for creating and managing the base Skia resources (contexts, surfaces) and connecting these to the GL render target. +The handler is attached to the canvas for its lifetime. On canvas disposal, all resources are released via the handler's cleanup logic. + +### Error Handling and Fallback + +If handler creation fails (e.g., due to missing OpenGL support or Skia initialization errors), the error is logged and the external handler mechanism is permanently disabled for the process. All subsequent canvases fall back to the default SWT rendering path. + +--- + +## Class Overview + +### `SkiaGlCanvasExtension` +**Package:** `org.eclipse.swt.internal.skia` + +The central class of the plugin. It connects an SWT `Canvas` widget to a Skia rendering surface +backed by an OpenGL framebuffer. + +**Inheritance:** +``` +GLCanvasExtension + └── GLPaintEventInvoker + └── SkiaGlCanvasExtension (implements ISkiaCanvasExtension, IExternalCanvasHandler) +``` + +**Responsibilities:** +- Creates and manages the Skia `DirectContext` (OpenGL-backed Skia GPU context). +- Creates and re-creates the `BackendRenderTarget` and `Surface` on canvas resize. +- Maintains a `lastImage` snapshot after each frame so that unchanged areas can be restored + cheaply without re-executing paint listeners. +- Drives the paint event loop by constructing a `SkiaGC`, wrapping it in a `GCExtension`, and + sending a paint `Event` to all registered SWT paint listeners. + +**Key fields:** +- `skijaContext` — Skia `DirectContext` for OpenGL, created once at construction time. +- `renderTarget` / `surface` — recreated on each canvas resize; `surface` is the drawable Skia canvas. +- `lastImage` — snapshot of the last fully rendered frame, used to restore unchanged areas on partial redraws. +- `redrawCommands` — list of pending redraw areas accumulated between paint cycles; `null` area means full repaint. +- `resources` — shared `SkiaResources` instance for fonts, images, and colors. +- `scaler` — `DpiScalerUtil` for automatic HiDPI coordinate scaling. + +--- + +### `GLCanvasExtension` +**Package:** `org.eclipse.swt.opengl` + +Duplicates the functionality of `GLCanvas` but without extending the `Canvas` widget hierarchy. +It manages the OpenGL context, pixel format setup, and provides methods like `setCurrent()`, +`swapBuffers()`, `isCurrent()`, and `getGLData()`. Disposal of the OpenGL context is handled +via an `SWT.Dispose` listener on the canvas. + +--- + +### `GLPaintEventInvoker` +**Package:** `org.eclipse.swt.opengl` + +Extends `GLCanvasExtension` and provides the mechanism to invoke paint events within an OpenGL +context. It receives a `Consumer` (the paint event sender), calls the abstract `doPaint()` +method (implemented by subclasses like `SkiaGlCanvasExtension`), and then calls `swapBuffers()`. + +It also manages a `redrawTriggered` flag to schedule additional redraws when needed. + +--- + +### `ISkiaCanvasExtension` +**Package:** `org.eclipse.swt.internal.skia` + +Interface implemented by `SkiaGlCanvasExtension` and used by `SkiaGC`. It provides the GC with +access to Skia-specific services without exposing the full extension class. + +**Methods:** +- `getSurface()` — returns the current Skia `Surface` for drawing. +- `getResources()` — returns the shared `SkiaResources` instance. +- `createSupportSurface(int w, int h)` — creates a temporary off-screen GPU surface (used for + cached text rendering). +- `getScaler()` — returns the `DpiScalerUtil` for logical-to-physical coordinate conversion. + +--- + +### `SkiaGC` +**Package:** `org.eclipse.swt.internal.graphics` + +Implements `IExternalGC` — the SWT-internal interface that maps all GC drawing calls to a +non-native backend. `SkiaGC` translates every SWT drawing operation into Skija canvas calls. +Drawing operations (stroke and fill) are delegated to `SkiaPaintManager`, which configures +the Skija `Paint` object with color, alpha, line style, pattern shaders, and XOR blend mode. + +**Canvas state management (save/restore stack):** + +Skija's `Canvas` uses a state stack. Each `save()` pushes the current clip and transform state; +`restore()` pops it. `SkiaGC` manages this stack as follows: +- On construction: `canvas.save()` captures the baseline state (`initialSaveCount`). +- On `dispose()`: `canvas.restoreToCount(initialSaveCount)` undoes all state changes made during + the lifetime of this GC, including all clipping and transform layers. +- Each `setClipping(...)` call checks the `isClipSet` flag; if a previous clip was set, it calls + `canvas.restore()` to remove that clip layer. Then it calls `canvas.save()` followed by the + new clip call to push a fresh clip layer and sets `isClipSet = true`. +- `setTransform(...)` calls `canvas.save()` after applying the matrix so that subsequent clip + operations are stacked on top of the transform independently. + +**SWT-to-Skija coordinate mapping:** +- All SWT coordinates are in logical (device-independent) pixels. +- `DpiScalerUtil.autoScaleUp()` converts logical pixel values to physical pixels before every Skija + draw call. +- `RectangleConverter.createScaledRectangle(scaler, x, y, w, h)` scales a bounding rectangle and + converts it to `io.github.humbleui.types.Rect`. +- `RectangleConverter.getScaledOffsetValue(scaler, lineWidth)` computes a 0.5-pixel sub-pixel + offset applied to stroke operations to prevent anti-aliasing blur when drawing on integer pixel + boundaries with odd-width strokes. + +**Image handling:** +- SWT `Image` objects are converted to Skija `Image` objects on demand via + `SwtToSkiaImageConverter.convertSWTImageToSkijaImage()`. +- Converted images are cached in `SkiaResources` by `(SWT Image identity, version, zoom)` key. + The version field invalidates the cache entry when a GC is created on the SWT image, then we expect an image modification. +- The pixel format of the SWT image is detected and mapped to the corresponding Skija `ColorType`. + Unsupported or ambiguous formats fall back to a full RGBA conversion via `RGBAEncoder`. + +--- + +### `SkiaPaintManager` +**Package:** `org.eclipse.swt.internal.graphics` + +Manages the creation and configuration of Skija `Paint` objects for all drawing operations in +`SkiaGC`. Provides two main entry points: + +- `performDraw(Consumer)` — configures a `Paint` for stroke operations (line width, line + cap, line style / dash pattern, foreground color, foreground pattern shader, XOR blend mode). +- `performDrawFilled(Consumer)` — configures a `Paint` for fill operations (background + color, background pattern shader, XOR blend mode). + +**Pattern handling:** +- `convertSWTPatternToSkijaShader(Pattern)` converts an SWT `Pattern` to a Skija `Shader`: + - Gradient patterns become `Shader.makeLinearGradient(...)`. + - Image patterns are converted via `SwtToSkiaImageConverter` and become `image.makeShader(FilterTileMode.REPEAT)`. + +--- + +### `SkiaTextDrawing` +**Package:** `org.eclipse.swt.internal.graphics` + +Static utility class responsible for drawing text strings onto the Skia surface. Supports two +modes controlled by the `USE_TEXT_CACHE` flag: +- `drawTextBlobWithCache` — renders text into a cached off-screen image and reuses it for + identical text/font/color combinations (default, enabled). +- `drawTextBlobNoCache` — renders text directly onto the surface without caching (debug mode). + +Handles multi-line text (split by delimiters), transparent backgrounds, and anti-aliasing settings. + +--- + +### `SwtToSkiaImageConverter` +**Package:** `org.eclipse.swt.internal.graphics` + +Converts SWT `Image` objects to Skija `Image` objects. Uses `SkiaResources` for caching: if a +cached Skija image already exists for the given SWT image identity, version, and zoom level, it +is returned directly. Otherwise, the SWT `ImageData` pixel buffer is extracted and mapped to the +appropriate Skija `ColorType`. + +--- + +### `SkijaToSwtImageConverter` +**Package:** `org.eclipse.swt.internal.graphics` + +Converts Skija `Image` objects back to SWT `Image` objects (used when SWT code needs an SWT +image from Skia-rendered content). + +--- + +### `SkiaColorConverter` +**Package:** `org.eclipse.swt.internal.graphics` + +Utility class for converting between SWT `Color` values and Skija integer color representations. +Provides methods for standard conversion, conversion with alpha, and color inversion. + +--- + +### `SkiaPathConverter` +**Package:** `org.eclipse.swt.internal.graphics` + +Converts SWT `Path` objects to Skija `Path` objects, applying DPI scaling to all coordinates. + +--- + +### `SkiaTransformConverter` +**Package:** `org.eclipse.swt.internal.graphics` + +Converts SWT `Transform` objects to Skija `Matrix33` representations for canvas transformations. + +--- + +### `SkiaRegionCalculator` +**Package:** `org.eclipse.swt.internal.graphics` + +Implements `AutoCloseable`. Calculates and manages Skia clip regions from SWT `Region` objects. + +--- + +### `RectangleConverter` +**Package:** `org.eclipse.swt.internal.graphics` + +Static utility class for converting and scaling SWT `Rectangle` coordinates to Skija `Rect` +objects. Provides methods such as `createScaledRectangle()`, `createScaledRectangleWithOffset()`, +`scaleUpRectangle()`, and `getScaledOffsetValue()` — all of which use `DpiScalerUtil` for +physical-to-logical pixel conversion. + +--- + +### `RGBAEncoder` +**Package:** `org.eclipse.swt.internal.graphics` + +Fallback pixel format converter. When the SWT image pixel format cannot be directly mapped to a +Skija `ColorType`, this class performs a full RGBA conversion of the pixel data. + +--- + +### `DpiScalerUtil` +**Package:** `org.eclipse.swt.internal.skia` + +Implements `IDpiScaler`. Wraps a platform-specific `DpiScaler` (or any `IDpiScaler`) and provides +all DPI-aware scaling methods used throughout the Skia plugin: `autoScaleUp(int)`, +`autoScaleUp(float)`, `autoScaleDown(float)`, `autoScaleDown(float[])`, `getZoomedFontSize(int)`, +`scaleSize(int, int)`, and `scaleSurfaceSize(int, int)`. The underlying zoom factor is obtained +from the wrapped `IDpiScaler.getNativeZoom()`. + +--- + +### `SkiaResources` +**Package:** `org.eclipse.swt.internal.skia` + +Centralizes all shared Skia resources for a single canvas widget. One instance is created per +`SkiaGlCanvasExtension` and is shared with the `SkiaGC` created for each paint event. + +**Responsibilities:** +- **Font management:** Converts SWT `Font` objects to Skija `Font` objects and caches them by + `FontProperties` (family name, weight, height, italic flag). Font family resolution uses a + best-fit scoring algorithm against all font families registered in the system `FontMgr`. + If no match is found, the system default font is used as a fallback. Unresolvable names are + added to the `unknownFonts` set to avoid repeated lookup overhead. +- **Image cache:** LRU cache (`LruImageCache`) of Skija `Image` objects keyed by + `ImageKey(SWT Image identity, version, zoom level)`. Maximum capacity: 256 entries. + Evicted images are automatically closed. +- **Text image cache:** LRU cache of pre-rendered text images keyed by + `ImageTextKey(text, font properties, transparency flag, background color, foreground color, antialias)`. + Maximum capacity: 512 entries. Used by `SkiaTextDrawing.drawTextBlobWithCache`. +- **Text split cache:** Caches pre-processed text split arrays (`SplitsTextCache` key) covering + tab expansion, delimiter splitting, and mnemonic stripping to avoid repeated string processing. +- **Color management:** Stores the current foreground and background `Color` for the GC. + `resetBaseColors()` is called on `SkiaGC.dispose()` to clear the references and avoid + retaining disposed colors. +- **Cleanup:** All cached Skija resources are explicitly closed in `resetResources()`, which + is registered as both an `SWT.Dispose` and `SWT.ZoomChanged` listener on the canvas widget. + +--- + +### Caret Support (not yet implemented) + +Caret rendering on the Skia surface is **not yet implemented**. The `SkiaGlCanvasExtension.doPaint()` +method contains comments indicating where caret drawing would be integrated — after the image +snapshot is taken, so that a blinking caret could be redrawn by restoring the last image and +painting the caret on top without re-executing paint listeners. Full caret support requires +additional modifications to `Canvas` and `Caret`. + +--- + +### `SkiaCanvasFactory` +**Package:** `org.eclipse.swt.internal.canvasext` + +Implements `IExternalCanvasFactory`. This is the OSGi service registered via the service loader +to plug the Skia backend into the SWT canvas extension framework. + +When an SWT `Canvas` with the `SWT.SKIA` style is created, the framework calls +`createCanvasExtension(Canvas)` on this factory. If Skia initialization fails (for example, +because the platform does not support OpenGL), the error is logged via `Logger.logException()` +and Skia is permanently disabled for the current process (`skiaFailedWithErrors = true`), so +that subsequent canvas creations do not repeatedly attempt and fail to initialize Skia. + +--- + +### `SkiaFontMetrics` +**Package:** `org.eclipse.swt.internal.graphics` + +Wraps a Skija `FontMetrics` object and implements `IExternalFontMetrics`. Provides ascent, descent, +height, leading, and average character width values to the SWT `FontMetrics` API via +`FontMetricsExtension`. All values are scaled down from physical to logical pixels via `DpiScalerUtil`. + +--- + +## Rendering Pipeline + +``` +SWT widget calls canvas.redraw() + │ + ▼ +SkiaGlCanvasExtension.redrawTriggered() + └── adds RedrawCommand to queue, delegates to GLPaintEventInvoker + │ + ▼ +GLPaintEventInvoker.paint(consumer, wParam, lParam) + ├── calls doPaint(consumer) → dispatched to SkiaGlCanvasExtension + ├── calls swapBuffers() + └── if redrawTriggered flag set → canvas.redraw() (schedule next frame) + │ + ▼ +SkiaGlCanvasExtension.doPaint(paintEventSender) + ├── Determine dirty area (union of all pending RedrawCommands) + │ └── null area → full repaint + ├── If partial repaint: draw lastImage onto surface (restore unchanged content) + ├── surface.getCanvas().save() + clipRect(dirtyArea) + ├── surface.getCanvas().clear(backgroundColor) + ├── executePaintEvents(paintEventSender, dirtyArea) + │ ├── create SkiaGC (saves initial canvas state) + │ ├── wrap in GCExtension, attach to Event + │ └── invoke paintEventSender → SWT paint listeners fire + │ └── listeners call gc.drawXxx() / gc.fillXxx() + │ └── SkiaPaintManager.performDraw() / performDrawFilled() + │ ├── configure Paint (color, style, shader) + │ └── Skija Canvas.drawXxx() calls + ├── SkiaGC.dispose() → canvas.restoreToCount(initialSaveCount) + ├── createLastImageSnapshot() — cache frame for next partial repaint + └── skijaContext.flush() — submit all buffered GPU commands to OpenGL +``` + +--- + +## DPI and Coordinate Scaling + +Since Skia requires physical pixels, the conversion from SWT logical pixels is handled +transparently by `DpiScalerUtil` (which wraps a platform-specific `DpiScaler` / `IDpiScaler`) +and helper methods in `RectangleConverter`: + +- `DpiScalerUtil.autoScaleUp(int)` / `DpiScalerUtil.autoScaleUp(float)` — converts a logical pixel value to physical pixels. +- `DpiScalerUtil.autoScaleDown(float)` / `DpiScalerUtil.autoScaleDown(float[])` — converts physical pixel values back to logical pixels. +- `DpiScalerUtil.getZoomedFontSize(int)` — converts a font size from points to zoomed physical pixels. +- `RectangleConverter.scaleUpRectangle(DpiScalerUtil, Rectangle)` — scales a full rectangle to physical coordinates. + +--- + +## Architecture Decision: `Canvas`, `GCExtension` and `FontMetricsExtension` + +In order to minimize API modifications, the only new API is the field in SWT, SKIA. So the classical canvas can be used, just the style SWT.SKIA must be set. This minimizes the modifications, which are necessary for application developers to use the Skia feature. + +Only small modifications to the canvas class were necessary. + +SWT's `GC` and `FontMetrics` classes are effectively sealed types — they are tightly coupled to +the native platform handles and are not designed for subclassing by external rendering backends. + +Rather than refactoring `GC` into a wrapper class with a delegate (which would require +changes across the entire SWT repository), the Skia integration introduces two minimal subclasses: + +- **`GCExtension extends GC`**: Overrides all drawing and state methods of `GC` to delegate them + to an `IExternalGC` implementation (i.e. `SkiaGC`). Native handle access methods throw + `IllegalStateException` to prevent misuse. `GCExtension` is annotated `@noreference` so that + it remains an internal implementation detail. + +- **`FontMetricsExtension extends FontMetrics`**: Overrides all measurement methods to delegate + to an `IExternalFontMetrics` implementation (i.e. `SkiaFontMetrics`). + +**Rationale:** This approach minimizes the impact on the SWT codebase. No modifications to the +existing SWT widget hierarchy, paint dispatch logic, or public API are required. The Skia backend +plugs in entirely through the `SkiaCanvasFactory` service registration and the two extension +classes. This makes the feature self-contained and easy to enable or disable at the OSGi bundle +level. Further architectural refinements (e.g. a proper wrapper `GC` delegate model) can follow +in a later iteration. + +--- + +## Widgets with Limited or No Support + +The Skia backend operates at the `Canvas` level. Higher-level SWT widgets that manage their own +native painting (such as `StyledText`, `Table`, and `Tree`) do not currently use the Skia +rendering path. These widgets paint through native OS controls or through their own internal GC +usage, which bypasses the canvas extension mechanism. `ExternalCanvasHandler` explicitly excludes +`StyledText` and `Decorations` subclasses. + +--- + +## Code Review + +The following findings were identified by reviewing the source code referenced in this +architecture document. They are grouped by severity. + +### Critical — Resource Leaks and Native Memory + +| # | File / Location | Finding | +|---|---|---| +| C1 | `SkiaGlCanvasExtension.onDispose()` (line 94) | **`skijaContext` is intentionally never closed** ("freezes the app"). This indicates a deeper lifecycle or threading issue with the OpenGL context. The `DirectContext` holds GPU resources; leaking it on every `Canvas` dispose accumulates native memory. The root cause of the freeze should be investigated and the context should be properly closed. | +| C2 | `SkiaGlCanvasExtension.onResize()` (line 107) | **Redundant `DpiScalerUtil` allocation on every resize.** A new `DpiScalerUtil` is constructed from `resources.getScaler()` on every resize event, although the field `this.scaler` already exists and wraps the same `DpiScaler`. The local `util` variable should be replaced with `this.scaler`. The same issue occurs in `doPaint()` (line 198). | +| C3 | `SkiaGC.drawPath()` (line 332–334) | **Hardcoded miter limit of 100 000 and anti-alias off.** The caller's `lineJoin`, `miterLimit`, and `antialias` settings are completely ignored; instead, fixed values are applied (`setStrokeMiter(100000)`, `setAntiAlias(false)`, `setStrokeCap(BUTT)`). This breaks the GC contract for any consumer who sets these attributes before calling `drawPath()`. | +| C4 | `SkiaPaintManager.performDraw()` (line 51) | **`lineJoin` is never applied.** The `Paint` object is configured with stroke cap and width, but `paint.setStrokeJoin(...)` is never called. Consequently the Skia default join (MITER) is always used, regardless of what `gc.setLineJoin(SWT.JOIN_ROUND)` requests. Similarly, `dashOffset` and `miterLimit` stored in `SkiaGC` are never forwarded to `createPathEffect` or `paint.setStrokeMiter(...)`. | +| C5 | `SkiaPaintManager.convertSWTPatternToSkijaShader()` (line 166–168) | **Image-pattern Shader created from a cached image that is immediately closed.** The `try-with-resources` block closes the Skija `Image` obtained from `SwtToSkiaImageConverter` right after `makeShader()`. If Skia's `Shader` retains only a reference to the underlying pixel data (and does not deep-copy it), this produces a use-after-free on the GPU. Whether this is safe depends on Skija internals and should be verified or the image should be kept alive as long as the Shader is in use. | +| C6 | `SkiaTextDrawing.drawTextBlobWithCache()` (line 227) | **`makeImageSnapshot()` result is cached but never explicitly closed outside the cache.** The snapshot is stored in the `textImageCache` LRU cache and will be closed when evicted. However, if `cacheTextImage()` replaces an existing entry (same key), the old image is closed via `old.close()` — but `textImageCache.get(key)` is called first, which promotes the entry in the LRU and may interfere with the eviction check. Verify that the double-close guard (`if (!old.isClosed())`) is always reached. | + +### High — Correctness and Behavioral Bugs + +| # | File / Location | Finding | +|---|---|---| +| H1 | `SkiaGC.drawPolygon()` (line 370–373) | **Mutates the caller's `pointArray` in place** by decrementing x-coordinates for the `SWT.MIRRORED` style and then incrementing them back afterwards. If the paint listener is concurrent or the array is reused, this is a race condition. A defensive copy should be used instead of modifying the input array. | +| H2 | `SkiaGC.setTransform()` (line 595–603) | **Cumulative canvas saves without matching restores.** Each call to `setTransform()` calls `canvas.save()` but never calls `canvas.restore()` for the previous transform layer. If `setTransform()` is called multiple times, the save stack grows unboundedly until `dispose()` restores to `initialSaveCount`. While not a memory leak (Skia's stack is lightweight), it is semantically incorrect — subsequent `setClipping()` operations interact with each stacked layer in unexpected ways. | +| H3 | `SkiaGC.setClipping(Path)` (line 896–918) | **`currentClipBounds` and `currentClipRegion` are not cleared** when clipping is set via a `Path`. After calling `setClipping(Path)`, a subsequent call to `getClipping()` would return stale bounds from a previous `setClipping(Rectangle)` call. | +| H4 | `SkiaGC.executeTextDraw()` (line 284) | **Early-return check uses physical surface size against logical coordinates.** `surface.getWidth()` returns physical pixels, but `x` and `y` are logical (unscaled) SWT coordinates. On a 150 % DPI display, text at logical x = 800 would be rejected even though the physical surface is 1200 px wide. The comparison should either scale the surface bounds down or the coordinates up. | +| H5 | `SkiaResources.findBestFit()` (line 233–264) | **Font matching returns false positives.** The algorithm scores each system font family by counting how many whitespace-separated tokens of the requested font name appear in the family name. A request for `"Segoe"` would match `"Segoe UI"` (score 1) but also `"Segoe MDL2 Assets"` (score 1) with the same score. The first family that reaches the top score wins — order depends on `FontMgr` enumeration. A stricter scoring (e.g. exact-match bonus, prefix matching, length penalty) would be more robust. | +| H6 | `SkiaResources.textExtent()` (line 463–467) | **`flags` parameter is ignored.** The `flags` argument (e.g. `SWT.DRAW_MNEMONIC`, `SWT.DRAW_DELIMITER`, `SWT.DRAW_TAB`) is accepted but never evaluated. The measurement always returns the raw single-line text extent, even when the caller expects multi-line measurement. | +| H7 | `SkiaResources.expandTabs()` (line 455) | **Per-character measurement via `String.valueOf(ch)` creates a new `String` object for every character.** The comment above it even says "measureTextWidth avoids creating an intermediate String object per character" — but it does exactly that. For long strings this causes excessive allocation pressure. Consider measuring runs of non-tab characters as a single string. | +| H8 | `ExternalCanvasHandler.createHandler()` (line 128) | **Catches `Throwable`** (including `Error`, `OutOfMemoryError`, `StackOverflowError`). This is extremely broad. A fatal JVM error should not be silently swallowed with `FAILED_WITH_ERRORS = true`. Consider catching `Exception` only, and rethrowing `Error` subclasses. The same issue exists in `SkiaCanvasFactory.createCanvasExtension()`. | + +### Medium — Design, Maintainability, and Performance + +| # | File / Location | Finding | +|---|---|---| +| M1 | `SkiaGC` (class level) | **1 258-line God class.** `SkiaGC` implements the entire `IExternalGC` interface in a single class with no delegation other than `SkiaPaintManager` for paint configuration. Methods like `copyArea`, `textLayoutDraw`, `fillGradientRectangle`, and `drawImage` with their complex logic should be extracted into focused helper classes (similar to how `SkiaTextDrawing` was extracted). | +| M2 | `SkiaGC.writeFile()` (line 226–239) | **Debug/test method left in production code.** The static `writeFile()` method writes a PNG snapshot to an arbitrary file path. It has no access protection and is documented as "Test method for drawing an image." This should be removed or moved to a test utility class. | +| M3 | `SkiaGC.getAdvanceWidth()` (line 787–803) | **Abuses `AtomicInteger` and `performDraw()` to measure text width.** `performDraw()` creates a full `Paint` object with stroke configuration just to measure a single character. The actual measurement only needs a `Paint` in FILL mode. A simpler approach would be to call `font.measureTextWidth()` directly. | +| M4 | `SkiaGC.getGCData()` (line 827–829) | **Returns `null`.** Any caller that depends on non-null `GCData` (which is common in SWT internals) will get a `NullPointerException`. If `GCData` is not applicable for Skia, consider returning a dummy instance or throwing `UnsupportedOperationException`. | +| M5 | `DpiScalerUtil` (constructors) | **Three constructors with overlapping concerns.** `DpiScalerUtil(IDpiScaler)`, `DpiScalerUtil(int)`, and `DpiScalerUtil(SkiaResources)` all set `this.scaler`. The `SkiaResources` constructor just delegates to `resources.getScaler()`, so the caller could pass the `IDpiScaler` directly. Multiple constructors increase the API surface without clear benefit. | +| M6 | `DpiScalerUtil.getZoomedFontSize()` (line 43–46) | **Calls `Display.getDefault()` from a utility class.** This couples the utility to the SWT display singleton and makes it untestable. The DPI value should be injected or obtained from the canvas that is already available in the call chain. | +| M7 | `SkiaResources` (field level) | **Caches (`fontCache`, `fontNameMapping`, `imageCache`, `textImageCache`, `cachedTextSplits`) grow without bounds (except the LRU caches).** `fontCache`, `fontNameMapping`, and `cachedTextSplits` are plain `HashMap` instances that never evict entries during the canvas lifetime. For long-running applications with many font or text variations this can become a memory issue. | +| M8 | `SkiaResources.getSkijaFont()` (line 160–162) | **Redundant cache lookup.** After `fontCache.put(props, f)` on line 158, line 162 does `return fontCache.get(props)` instead of simply `return cachedFont` (which at this point holds the same reference as `f`). In fact, `cachedFont` is not `null` on line 162 — this code is unreachable because the `if (cachedFont == null)` branch already returned on line 159. The dead code should be simplified. | +| M9 | `ExternalCanvasHandler` (line 25–26) | **Non-final static mutable fields (`FAILED_WITH_ERRORS`, `ExternalCanvasWasLogged`) with no synchronization.** These flags are written and read from the SWT UI thread, which is single-threaded in practice, but the class provides no documentation of this threading assumption. Also, `FAILED_WITH_ERRORS` does not follow Java naming conventions for non-constant fields. | +| M10 | `RectangleConverter` (line 52–61) | **Direct access to internal Skija field `_radii`.** `rect._radii` accesses a package-private or internal field of `RRect`. This is fragile and may break with Skija library updates. Use a public accessor if available. | +| M11 | `SkiaGC.copyArea(Image, int, int)` (line 689–724) | **Inefficient round-trip conversion.** The method creates a Skija snapshot, converts it to SWT `ImageData`, creates a new SWT `Image`, creates a native `GC`, draws the image, and disposes everything. This GC-on-GC approach is slow and allocates significant temporary resources. A direct Skija-to-Skija blit would be more efficient. | +| M12 | `SkiaGC.textLayoutDraw()` (line 1214–1238) | **Falls back to native GC rendering.** The method creates a temporary SWT `Image`, draws the `TextLayout` with a native `GC`, then converts and blits the result into the Skia surface. This means text layout rendering is always rasterized at 100 % zoom and then scaled, losing quality on HiDPI displays. A comment explaining why this fallback is needed and a TODO for a native Skia `TextLayout` implementation would be helpful. | + +### Low — Style and Minor Issues + +| # | File / Location | Finding | +|---|---|---| +| L1 | `SkiaGC` (line 82–85) | **TODO comment for `dashOffset` and `miterLimit`** has been present since initial commit. These fields are stored but never applied to the Skia `Paint`. Either implement them or remove the fields and throw `UnsupportedOperationException` in the setters. | +| L2 | `SkiaGlCanvasExtension` (line 181) | **Typo in comment:** `"if if we don't even have"` — double "if". | +| L3 | `ExternalCanvasHandler` (lines 44–65) | **Verbose boolean-returning methods.** `isDisabled()`, `isForcedEnabled()`, and `isLogActive()` can be simplified from `if (x != null) { return true; } return false;` to `return x != null;`. | +| L4 | `SkiaGC` (line 68) | **`logImageNullError` static flag** suppresses image-null errors after the first occurrence globally across all GC instances. This makes debugging difficult if the issue occurs in different contexts. Consider per-instance logging or a counter. | +| L5 | `SkiaPaintManager` (line 107) | **Unused parameter.** `getScaledPathFloats()` accepts a `DpiScalerUtil scaler` parameter but never uses it. The parameter should be removed. | +| L6 | `SkiaResources.replaceMnemonics()` (line 478–485) | **`lastIndexOf('&')` is computed but its result is only used to check `!= -1`.** The actual index is never used for underlining. This is currently just a check before calling `replaceAll("&", "")` — the `lastIndexOf` check is redundant since `replaceAll` on a string without `&` is a no-op. | +| L7 | Multiple files | **Inconsistent field-access style.** Some methods use `this.surface`, others use `surface` directly. Some use `getScaler()`, others use `this.dpiScalerUtil` or create a new `DpiScalerUtil`. A consistent convention should be established. | + +--- + +## Known Limitations and Open TODOs + +- **Mnemonic underlining:** `replaceMnemonics()` strips `&` prefix characters from text but does + not yet draw an underline beneath the mnemonic character. +- **No Caret support currently** (caret rendering is not yet implemented; see comments in `SkiaGlCanvasExtension.doPaint()`) +- **No Skia TextLayout** + +--- \ No newline at end of file diff --git a/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/canvasext/Logger.java b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/canvasext/Logger.java new file mode 100644 index 00000000000..67b4b369475 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/canvasext/Logger.java @@ -0,0 +1,9 @@ +package org.eclipse.swt.internal.canvasext; + +public class Logger { + + public static void logException(Throwable t) { + t.printStackTrace(System.err); + } + +} diff --git a/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/canvasext/SkiaCanvasFactory.java b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/canvasext/SkiaCanvasFactory.java new file mode 100644 index 00000000000..e4bc09f726a --- /dev/null +++ b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/canvasext/SkiaCanvasFactory.java @@ -0,0 +1,41 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.swt.internal.canvasext; + +import org.eclipse.swt.internal.skia.SkiaGlCanvasExtension; +import org.eclipse.swt.widgets.Canvas; + +/** + * Provides a factory for creating Skia-based canvas extensions. This factory is + * used by org.eclipse.swt.internal.canvasext.ExternalCanvasHandler with the + * resource loader to create canvas extensions when the SKIA style is specified. + */ +public class SkiaCanvasFactory implements IExternalCanvasFactory { + + private static boolean skiaFailedWithErrors = false; + + @Override + public IExternalCanvasHandler createCanvasExtension(Canvas c) { + + if (skiaFailedWithErrors) { + return null; + } + + try { + return new SkiaGlCanvasExtension(c); + } catch (final Throwable t) { + // Skia initialization failed; log and disable Skia for all future calls + Logger.logException(t); + skiaFailedWithErrors = true; + return null; + } + } +} diff --git a/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/RGBAEncoder.java b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/RGBAEncoder.java new file mode 100644 index 00000000000..7553a68ed2f --- /dev/null +++ b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/RGBAEncoder.java @@ -0,0 +1,112 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.swt.internal.graphics; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.RGB; + +/** + * Encodes an ImageLoader's first ImageData to a raw RGBA byte array. + * Handles direct and indexed color images, and all SWT transparency types. + */ +public final class RGBAEncoder { + + /** + * Returns a byte array of RGBA pixels (width * height * 4). + */ + public static byte[] encode(ImageData data) { + + int colorType; + + final var width = data.width; + final var height = data.height; + final var transparencyType = data.getTransparencyType(); + if (data.palette.isDirect) { + if (transparencyType == SWT.TRANSPARENCY_ALPHA) { + colorType = 6; // RGBA + } else { + colorType = 2; // RGB + } + } else { + colorType = 3; // Indexed + } + if (!(colorType == 2 || colorType == 3 || colorType == 6)) { + SWT.error(SWT.ERROR_INVALID_IMAGE); + } + + + + final byte[] rgba = new byte[width * height * 4]; + if (colorType == 3) { + // Indexed color + final RGB[] palette = data.palette.getRGBs(); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + final int pixel = data.getPixel(x, y); + final RGB rgb = palette[pixel]; + final int offset = (y * width + x) * 4; + rgba[offset] = (byte) rgb.red; + rgba[offset + 1] = (byte) rgb.green; + rgba[offset + 2] = (byte) rgb.blue; + int a = 255; + if (transparencyType == SWT.TRANSPARENCY_ALPHA && data.alphaData != null) { + a = data.alphaData[y * width + x] & 0xFF; + } else if (transparencyType == SWT.TRANSPARENCY_PIXEL && pixel == data.transparentPixel) { + a = 0; + } else if (transparencyType == SWT.TRANSPARENCY_MASK && data.maskData != null) { + final int maskOffset = y * data.width + x; + final int maskByte = (data.maskData[maskOffset / 8] >> (7 - (maskOffset % 8))) & 0x1; + a = maskByte == 0 ? 0 : 255; + } + rgba[offset + 3] = (byte) a; + } + } + } else { + // Direct color + final int redMask = data.palette.redMask; + final int redShift = data.palette.redShift; + final int greenMask = data.palette.greenMask; + final int greenShift = data.palette.greenShift; + final int blueMask = data.palette.blueMask; + final int blueShift = data.palette.blueShift; + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + final int pixel = data.getPixel(x, y); + final int offset = (y * width + x) * 4; + int r = pixel & redMask; + r = (redShift < 0) ? r >>> -redShift : r << redShift; + int g = pixel & greenMask; + g = (greenShift < 0) ? g >>> -greenShift : g << greenShift; + int b = pixel & blueMask; + b = (blueShift < 0) ? b >>> -blueShift : b << blueShift; + rgba[offset] = (byte) r; + rgba[offset + 1] = (byte) g; + rgba[offset + 2] = (byte) b; + int a = 255; + if (transparencyType == SWT.TRANSPARENCY_ALPHA && data.alphaData != null) { + a = data.alphaData[y * width + x] & 0xFF; + } else if (transparencyType == SWT.TRANSPARENCY_PIXEL && pixel == data.transparentPixel) { + a = 0; + } else if (transparencyType == SWT.TRANSPARENCY_MASK && data.maskData != null) { + final int maskOffset = y * data.width + x; + final int maskByte = (data.maskData[maskOffset / 8] >> (7 - (maskOffset % 8))) & 0x1; + a = maskByte == 0 ? 0 : 255; + } else if (data.alpha != -1) { + a = data.alpha & 0xFF; + } + rgba[offset + 3] = (byte) a; + } + } + } + return rgba; + } +} diff --git a/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/RectangleConverter.java b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/RectangleConverter.java new file mode 100644 index 00000000000..fe29ec4f56c --- /dev/null +++ b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/RectangleConverter.java @@ -0,0 +1,77 @@ +package org.eclipse.swt.internal.graphics; + +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.internal.skia.DpiScalerUtil; + +import io.github.humbleui.types.RRect; +import io.github.humbleui.types.Rect; + +public class RectangleConverter { + + + public static Rect createScaledRectangle(DpiScalerUtil sc,Rectangle r) { + return createScaledRectangle( sc,r.x, r.y, r.width, r.height); + } + + public static Rectangle scaleUpRectangle(DpiScalerUtil sc, Rectangle rectangle) { + return new Rectangle(sc.autoScaleUp(rectangle.x), sc.autoScaleUp(rectangle.y), sc.autoScaleUp(rectangle.width), + sc.autoScaleUp(rectangle.height)); + } + + public static Rect createScaledRectangle( DpiScalerUtil sc, int x, int y, int width, int height) { + final var r = scaleUpRectangle(sc,new Rectangle(x, y, width, height)); + return new Rect(r.x, r.y, r.width + r.x, r.height + r.y); + } + + public static float getScaledOffsetValue(DpiScalerUtil sc, int lineWidth) { + + final boolean isDefaultLineWidth = lineWidth == 0; + if (isDefaultLineWidth) { + return 0.5f; + } + final int effectiveLineWidth = sc.autoScaleUp(lineWidth); + if (effectiveLineWidth % 2 == 1) { + return sc.autoScaleUp(0.5f); + } + return 0f; + } + + public static Rect offsetRectangle(DpiScalerUtil sc, int lineWidth, Rect rect) { + + + final float scaledOffsetValue = getScaledOffsetValue(sc, lineWidth); + final float widthHightAutoScaleOffset = sc.autoScaleUp(1) - 1.0f; + if (scaledOffsetValue > 0f) { + return new Rect(rect.getLeft() + 0.5f, rect.getTop() + 0.5f, + rect.getRight() + 0.5f + widthHightAutoScaleOffset, + rect.getBottom() + 0.5f + widthHightAutoScaleOffset); + } + return rect; + } + + public static RRect offsetRectangle(DpiScalerUtil sc, int lineWidth,RRect rect) { + + final float scaledOffsetValue =getScaledOffsetValue(sc, lineWidth); + final float widthHightAutoScaleOffset = sc.autoScaleUp(1) - 1.0f; + if (scaledOffsetValue != 0f) { + return new RRect(rect.getLeft() + scaledOffsetValue, rect.getTop() + scaledOffsetValue, + rect.getRight() + scaledOffsetValue + widthHightAutoScaleOffset, + rect.getBottom() + scaledOffsetValue + widthHightAutoScaleOffset, rect._radii); + } + return rect; + } + + public static Rect createScaledRectangleWithOffset(DpiScalerUtil scaler, int lineWidth, int x, int y, + int width, int height) { + final var rect = createScaledRectangle(scaler, x, y, width, height); + return offsetRectangle(scaler, lineWidth, rect); + } + + + public static RRect createScaledRoundRectangle(DpiScalerUtil sc,int x, int y, int width, int height, float arcWidth, float arcHeight) { + return new RRect(sc.autoScaleUp(x), sc.autoScaleUp(y), sc.autoScaleUp(x + width), + sc.autoScaleUp(y + height), + new float[] { sc.autoScaleUp(arcWidth), sc.autoScaleUp(arcHeight) }); + } + +} diff --git a/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkiaColorConverter.java b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkiaColorConverter.java new file mode 100644 index 00000000000..d663194a260 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkiaColorConverter.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * Contributors: + * SAP SE and others - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.internal.graphics; + +import org.eclipse.swt.graphics.Color; + +public class SkiaColorConverter { + + public static int convertSWTColorToSkijaColor(Color swtColor) { + // extract RGB-components + final int red = swtColor.getRed(); + final int green = swtColor.getGreen(); + final int blue = swtColor.getBlue(); + final int alpha = swtColor.getAlpha(); + + // create ARGB 32-Bit-color + final int skijaColor = (alpha << 24) | (red << 16) | (green << 8) | blue; + + return skijaColor; + } + + public static int convertSWTColorToSkijaColor(Color swtColor, int alphaOverride) { + // extract RGB-components + final int red = swtColor.getRed(); + final int green = swtColor.getGreen(); + final int blue = swtColor.getBlue(); + final int alpha = alphaOverride; + + // create ARGB 32-Bit-color + final int skijaColor = (alpha << 24) | (red << 16) | (green << 8) | blue; + + return skijaColor; + } + + public static int invertSWTColorToInt(Color swtColor) { + + // extract RGB-components + final int red = swtColor.getRed(); + final int green = swtColor.getGreen(); + final int blue = swtColor.getBlue(); + final int alpha = swtColor.getAlpha(); + + // create ARGB 32-Bit-color + final int skijaColor = (alpha << 24) | ((0xFF - red) << 16) | ((0xFF - green) << 8) | (0xFF - blue); + + return skijaColor; + + } + +} diff --git a/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkiaFontMetrics.java b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkiaFontMetrics.java new file mode 100644 index 00000000000..623927f2bd2 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkiaFontMetrics.java @@ -0,0 +1,78 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.swt.internal.graphics; + +import java.util.Objects; + +import org.eclipse.swt.internal.canvasext.IExternalFontMetrics; +import org.eclipse.swt.internal.skia.DpiScalerUtil; + +public class SkiaFontMetrics implements IExternalFontMetrics { + + private final io.github.humbleui.skija.FontMetrics m; + private final DpiScalerUtil sc; + + SkiaFontMetrics(io.github.humbleui.skija.FontMetrics metrics, DpiScalerUtil dpiScaler) { + this.sc = dpiScaler; + this.m = metrics; + } + + @Override + public int getAscent() { + return sc.autoScaleDownToInt(((int) Math.ceil(Math.abs(m.getAscent() ) - Math.abs(m.getLeading()) ))); + } + + @Override + public double getAverageCharacterWidth() { + return sc.autoScaleDown(m.getAvgCharWidth()); + } + + @Override + public int getAverageCharWidth() { + return (int) Math.ceil(sc.autoScaleDown(m.getAvgCharWidth())); + } + + @Override + public int getDescent() { + return (int) Math.ceil(sc.autoScaleDown(m.getDescent())); + } + + @Override + public int getHeight() { + return (int) Math.ceil(sc.autoScaleDown(m.getHeight())); + } + + @Override + public int getLeading() { + return sc.autoScaleDownToInt( getHeight() - getAscent() - getDescent()); + } + + @Override + public int hashCode() { + return Objects.hash(m); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final SkiaFontMetrics other = (SkiaFontMetrics) obj; + return Objects.equals(m, other.m); + } + +} diff --git a/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkiaGC.java b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkiaGC.java new file mode 100644 index 00000000000..a7708738b28 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkiaGC.java @@ -0,0 +1,1290 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * Contributors: + * SAP SE and others - initial API and implementation + *******************************************************************************/ + +package org.eclipse.swt.internal.graphics; + +import java.io.File; +import java.io.FileOutputStream; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Device; +import org.eclipse.swt.graphics.Drawable; +import org.eclipse.swt.graphics.FontMetrics; +import org.eclipse.swt.graphics.FontMetricsExtension; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.GCData; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.LineAttributes; +import org.eclipse.swt.graphics.Path; +import org.eclipse.swt.graphics.Pattern; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.graphics.Region; +import org.eclipse.swt.graphics.TextLayout; +import org.eclipse.swt.graphics.Transform; +import org.eclipse.swt.internal.canvasext.IExternalGC; +import org.eclipse.swt.internal.canvasext.Logger; +import org.eclipse.swt.internal.skia.DpiScalerUtil; +import org.eclipse.swt.internal.skia.ISkiaCanvasExtension; +import org.eclipse.swt.internal.skia.SkiaResources; +import org.eclipse.swt.widgets.Display; + +import io.github.humbleui.skija.Canvas; +import io.github.humbleui.skija.ClipMode; +import io.github.humbleui.skija.EncoderPNG; +import io.github.humbleui.skija.FilterMipmap; +import io.github.humbleui.skija.FilterMode; +import io.github.humbleui.skija.FilterTileMode; +import io.github.humbleui.skija.GradientStyle; +import io.github.humbleui.skija.Matrix33; +import io.github.humbleui.skija.MipmapMode; +import io.github.humbleui.skija.Paint; +import io.github.humbleui.skija.PaintMode; +import io.github.humbleui.skija.PathEffect; +import io.github.humbleui.skija.PathFillMode; +import io.github.humbleui.skija.SamplingMode; +import io.github.humbleui.skija.Shader; +import io.github.humbleui.skija.Surface; +import io.github.humbleui.types.Rect; + +public class SkiaGC implements IExternalGC { + + private static boolean logImageNullError = true; + private final Surface surface; + private int alpha = 255; + private int antialias; + private boolean xorModeActive; + private int lineWidth = 1; + private int lineCap = SWT.CAP_FLAT; + private int lineStyle = SWT.LINE_SOLID; + private Pattern foregroundPattern; + private Pattern backgroundPattern; + + private int fillRule = SWT.FILL_EVEN_ODD; + private int lineJoin = SWT.JOIN_MITER; + private float[] lineDashes; + // TODO: implement ----------------------------------- + private float dashOffset; + private float miterLimit; + // ----------------------------------- + + private final org.eclipse.swt.widgets.Canvas canvas; + private final Display device; + + private Matrix33 currentTransform = Matrix33.IDENTITY; + + private SamplingMode interpolationMode = SamplingMode.DEFAULT; + + private boolean isClipSet; + private Rectangle currentClipBounds; + private Region currentClipRegion; + + private final ISkiaCanvasExtension skiaExtension; + private final SkiaResources resources; + private final int style; + private int textAntiAlias = SWT.ON; + + private final int initialSaveCount; + private final SkiaPaintManager paintManager; + private final DpiScalerUtil dpiScalerUtil; + + public SkiaGC(org.eclipse.swt.widgets.Canvas canvas, ISkiaCanvasExtension exst, int style) { + this.canvas = canvas; + device = canvas.getDisplay(); + this.surface = exst.getSurface(); + this.skiaExtension = exst; + this.resources = skiaExtension.getResources(); + this.style = style; + // Save the initial canvas state so it can be fully restored on dispose() + this.initialSaveCount = this.surface.getCanvas().save(); + this.paintManager = new SkiaPaintManager(this); + + this.dpiScalerUtil = new DpiScalerUtil(this.resources.getScaler()); + + this.currentClipBounds = canvas.getClientArea(); + + } + + @Override + public void dispose() { + + resources.resetBaseColors(); + // Restore all canvas state changes made during the lifetime of this GC, + // including any clipping or transform layers pushed after construction + surface.getCanvas().restoreToCount(initialSaveCount); + + } + + @Override + public Point textExtent(String string) { + return textExtent(string, SWT.NONE); + } + + @Override + public void setBackground(Color color) { + this.resources.setBackground(color); + } + + @Override + public void setForeground(Color color) { + this.resources.setForeground(color); + } + + @Override + public void fillRectangle(Rectangle rect) { + fillRectangle(rect.x, rect.y, rect.width, rect.height); + } + + private static void logImageNull(int[] positionData) { + if (logImageNullError) { + Logger.logException(new IllegalArgumentException( + "Image argument is null. Position and size data: " + java.util.Arrays.toString(positionData))); //$NON-NLS-1$ + logImageNullError = false; + } + } + + @Override + public void drawImage(Image image, int x, int y) { + + if (image == null) { + logImageNull(new int[] { x, y }); + return; + } + + final var imgBounds = image.getBounds(); + drawImage(image, 0, 0, imgBounds.width, imgBounds.height, x, y, imgBounds.width, imgBounds.height); + } + + @Override + public void drawImage(Image image, int destX, int destY, int destWidth, int destHeight) { + + if (image == null) { + logImageNull(new int[] { destX, destY, destWidth, destHeight }); + return; + } + + drawImage(image, 0, 0, image.getBounds().width, image.getBounds().height, destX, destY, destWidth, destHeight); + + } + + @Override + public void drawImage(Image image, int srcX, int srcY, int srcWidth, int srcHeight, int destX, int destY, + int destWidth, int destHeight) { + + if (image == null) { + logImageNull(new int[] { srcX, srcY, srcWidth, srcHeight, destX, destY, destWidth, destHeight }); + return; + } + + int sizeFactorInImage = Math.round(Math.max((float) destWidth / srcWidth, (float) destHeight / srcHeight)); + + if (sizeFactorInImage == 0) { + sizeFactorInImage = 1; + } + final Canvas canvas = surface.getCanvas(); + final int fac = sizeFactorInImage; + + paintManager.performDraw(paint -> { + paint.setAlpha(alpha); + paint.setAntiAlias(true); + canvas.drawImageRect( + SwtToSkiaImageConverter.convertSWTImageToSkijaImage(image, getScaler().autoScaleUp(100 * fac), + resources), + RectangleConverter.createScaledRectangle(getScaler(), srcX * fac, srcY * fac, srcWidth * fac, + srcHeight * fac), + RectangleConverter.createScaledRectangle(getScaler(), destX, destY, destWidth, destHeight), + this.interpolationMode, paint, false); + + }); + + } + + DpiScalerUtil getScaler() { + return this.dpiScalerUtil; + } + + /** + * Test method for drawing an image. + * + * @param str + * @param image + */ + static void writeFile(String str, io.github.humbleui.skija.Image image) { + final byte[] imageBytes = EncoderPNG.encode(image).getBytes(); + + final File f = new File(str); + if (f.exists()) { + f.delete(); + } + + try (FileOutputStream fis = new FileOutputStream(f)) { + fis.write(imageBytes); + } catch (final Exception e) { + Logger.logException(e); + } + } + + @Override + public void drawLine(int x1, int y1, int x2, int y2) { + final float scaledOffsetValue = RectangleConverter.getScaledOffsetValue(getScaler(), lineWidth); + + final var scaler = this.dpiScalerUtil; + paintManager.performDraw(paint -> surface.getCanvas().drawLine(scaler.autoScaleUp(x1) + scaledOffsetValue, + scaler.autoScaleUp(y1) + scaledOffsetValue, scaler.autoScaleUp(x2) + scaledOffsetValue, + scaler.autoScaleUp(y2) + scaledOffsetValue, paint)); + } + + @Override + public Color getForeground() { + return this.resources.getForeground(); + } + + @Override + public void drawText(String string, int x, int y) { + drawText(string, x, y, SWT.DRAW_DELIMITER | SWT.DRAW_TAB); + } + + @Override + public void drawText(String string, int x, int y, boolean isTransparent) { + int flags = SWT.DRAW_DELIMITER | SWT.DRAW_TAB; + if (isTransparent) { + flags |= SWT.DRAW_TRANSPARENT; + } + drawText(string, x, y, flags); + } + + @Override + public void drawText(String text, int x, int y, int flags) { + if (text == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + executeTextDraw(text, flags, x, y); + } + + private io.github.humbleui.skija.Font getSkiaFont() { + return this.resources.getSkiaFont(); + } + + private void executeTextDraw(String inputText, int flags, int x, int y) { + + if (this.surface.getWidth() < x || this.surface.getHeight() < y) { + return; + } + + final String splits[] = this.resources.getTextSplits(inputText, flags); + SkiaTextDrawing.drawText(this, splits, flags, x, y); + } + + SkiaPaintManager getPaintManager() { + return this.paintManager; + } + + @Override + public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) { + paintManager.performDraw(paint -> surface.getCanvas().drawArc(getScaler().autoScaleUp(x), + getScaler().autoScaleUp(y), getScaler().autoScaleUp(x + width), getScaler().autoScaleUp(y + height), + -startAngle, -arcAngle, false, paint)); + } + + @Override + public void drawFocus(int x, int y, int width, int height) { + paintManager.performDraw(paint -> { + final var scaledLineWidth = getScaler().autoScaleUp(lineWidth * 1F); + + try (var pe = PathEffect.makeDash(new float[] { 1.5f * scaledLineWidth, 1.5f * scaledLineWidth }, 0.0f)) { + paint.setPathEffect(pe); + surface.getCanvas().drawRect( + RectangleConverter.createScaledRectangleWithOffset(getScaler(), 0, x, y, width, height), paint); + } + + }); + } + + @Override + public void drawOval(int x, int y, int width, int height) { + paintManager.performDraw(paint -> surface.getCanvas().drawOval( + RectangleConverter.createScaledRectangleWithOffset(getScaler(), lineWidth, x, y, width, height), + paint)); + } + + @Override + public void drawPath(Path path) { + paintManager.performDraw(paint -> { + try (io.github.humbleui.skija.Path skijaPath = SkiaPathConverter.convertSWTPathToSkijaPath(path, + getScaler())) { + if (skijaPath == null) { + return; + } + // the stroke miter on GC is quite high. + paint.setStrokeMiter(1000); + surface.getCanvas().drawPath(skijaPath, paint); + } + }); + } + + @Override + public void drawPoint(int x, int y) { + paintManager.performDrawFilled(paint -> { + // points have always one pixel no matter the zoom... + // fill mode is right for it, not stroke, so we use performDrawFilled + paint.setColor(SkiaColorConverter.convertSWTColorToSkijaColor(getForeground())); + final var rec = RectangleConverter.scaleUpRectangle(getScaler(), new Rectangle(x, y, 1, 1)); + final var r = new Rect(rec.x, rec.y, rec.x + 1, rec.y + 1); + surface.getCanvas().drawRect(r, paint); + }); + } + + @Override + public void drawPolygon(int[] inputPointArray) { + + final int[] pointArray = Arrays.copyOf(inputPointArray, inputPointArray.length); + + if (pointArray == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + if (isDisposed()) { + SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); + } + if (pointArray.length < 6 || pointArray.length % 2 != 0) { + return; + } + + // Handle SWT.MIRRORED style (adjust x-coordinates if needed) + final int style = getStyle(); + final boolean mirrored = (style & SWT.MIRRORED) != 0; + final boolean adjustX = mirrored && lineWidth != 0 && lineWidth % 2 == 0; + if (adjustX) { + for (int i = 0; i < pointArray.length; i += 2) { + pointArray[i]--; + } + } + + paintManager.performDraw(paint -> { + // Create Skija path for the polygon + try (io.github.humbleui.skija.PathBuilder pathBuilder = new io.github.humbleui.skija.PathBuilder()) { + // Move to first point + pathBuilder.moveTo(getScaler().autoScaleUp(pointArray[0]), getScaler().autoScaleUp(pointArray[1])); + // Add lines to subsequent points + for (int i = 2; i < pointArray.length; i += 2) { + pathBuilder.lineTo(getScaler().autoScaleUp(pointArray[i]), + getScaler().autoScaleUp(pointArray[i + 1])); + } + pathBuilder.closePath(); + // Draw the polygon outline + + try (var path = pathBuilder.build()) { + if (path.isEmpty()) { + return; + } + surface.getCanvas().drawPath(path, paint); + } + } + }); + // Restore x-coordinates if mirrored + if (adjustX) { + for (int i = 0; i < pointArray.length; i += 2) { + pointArray[i]++; + } + } + } + + @Override + public void drawPolyline(int[] pointArray) { + paintManager.performDraw(paint -> surface.getCanvas().drawPolygon(convertToFloat(pointArray), paint)); + } + + private float[] convertToFloat(int[] array) { + final float[] arrayAsFloat = new float[array.length]; + for (int i = 0; i < array.length; i++) { + arrayAsFloat[i] = getScaler().autoScaleUp(array[i]); + } + return arrayAsFloat; + } + + @Override + public void drawRectangle(int x, int y, int width, int height) { + + paintManager.performDraw(paint -> { + surface.getCanvas().drawRect( + RectangleConverter.createScaledRectangleWithOffset(dpiScalerUtil, lineWidth, x, y, width, height), + paint); + }); + + } + + @Override + public void drawRectangle(Rectangle rect) { + drawRectangle(rect.x, rect.y, rect.width, rect.height); + } + + @Override + public void drawRoundRectangle(int x, int y, int width, int height, int arcWidth, int arcHeight) { + paintManager + .performDraw( + paint -> surface.getCanvas() + .drawRRect( + RectangleConverter + .offsetRectangle(getScaler(), lineWidth, + RectangleConverter.createScaledRoundRectangle(getScaler(), x, y, + width, height, arcWidth / 2.0f, arcHeight / 2.0f)), + paint)); + } + + @Override + public void drawString(String string, int x, int y) { + drawString(string, x, y, false); + } + + @Override + public void drawString(String string, int x, int y, boolean isTransparent) { + if (string == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + drawText(string, x, y, isTransparent); + } + + @Override + public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) { + paintManager.performDrawFilled(paint -> surface.getCanvas().drawArc(getScaler().autoScaleUp(x), + getScaler().autoScaleUp(y), getScaler().autoScaleUp(x + width), getScaler().autoScaleUp(y + height), + -startAngle, -arcAngle, true, paint)); + } + + @Override + public void fillGradientRectangle(int x, int y, int width, int height, boolean vertical) { + + boolean swapColors = false; + if (width < 0) { + x = Math.max(0, x + width); + width = -width; + if (!vertical) { + swapColors = true; + } + } + if (height < 0) { + y = Math.max(0, y + height); + height = -height; + if (vertical) { + swapColors = true; + } + } + final int x2 = vertical ? x : x + width; + final int y2 = vertical ? y + height : y; + + final Rect rect = RectangleConverter.createScaledRectangle(getScaler(), x, y, width, height); + int fromColor = SkiaColorConverter.convertSWTColorToSkijaColor(getForeground()); + int toColor = SkiaColorConverter.convertSWTColorToSkijaColor(getBackground()); + if (fromColor == toColor) { + paintManager.performDrawFilled(paint -> surface.getCanvas().drawRect(rect, paint)); + return; + } + if (swapColors) { + final int tempColor = fromColor; + fromColor = toColor; + toColor = tempColor; + } + + final var s = getScaler(); + performDrawGradientFilled(paint -> surface.getCanvas().drawRect(rect, paint), s.autoScaleUp(x), + s.autoScaleUp(y), s.autoScaleUp(x2), s.autoScaleUp(y2), fromColor, toColor); + } + + private void performDrawGradientFilled(Consumer operations, int x, int y, int x2, int y2, int fromColor, + int toColor) { + paintManager.performDraw(paint -> { + + try (Shader gradient = convertGradientRectangleToSkijaShader(x, y, x2 - x, y2 - y, false, fromColor, + toColor)) { + paint.setShader(gradient); + paint.setAntiAlias(true); + paint.setMode(PaintMode.FILL); + operations.accept(paint); + } + }); + } + + private static Shader convertGradientRectangleToSkijaShader(int x, int y, int width, int height, boolean vertical, + int fromColor, int toColor) { + + final var gs = new GradientStyle(FilterTileMode.REPEAT, true, null); + final Shader s = Shader.makeLinearGradient(x, y, x + width, y + height, new int[] { fromColor, toColor }, null, + gs); + + return s; + } + + @Override + public void fillOval(int x, int y, int width, int height) { + paintManager.performDrawFilled(paint -> surface.getCanvas() + .drawOval(RectangleConverter.createScaledRectangle(getScaler(), x, y, width, height), paint)); + } + + @Override + public void fillPath(Path path) { + paintManager.performDrawFilled(paint -> { + try (io.github.humbleui.skija.Path skijaPath = SkiaPathConverter.convertSWTPathToSkijaPath(path, + getScaler())) { + if (skijaPath == null) { + return; + } + skijaPath + .setFillMode(this.fillRule == SWT.FILL_EVEN_ODD ? PathFillMode.EVEN_ODD : PathFillMode.WINDING); + paint.setAntiAlias(this.antialias == SWT.ON); + surface.getCanvas().drawPath(skijaPath, paint); + } + }); + } + + @Override + public void fillPolygon(int[] pointArray) { + if (pointArray == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + if (isDisposed()) { + SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); + } + if (pointArray.length < 6 || pointArray.length % 2 != 0) { + return; + } + + paintManager.performDrawFilled(paint -> { + + try (var path = SkiaPathConverter.createPath(pointArray, fillRule, getScaler())) { + if (path == null || path.isEmpty()) { + return; + } + surface.getCanvas().drawPath(path, paint); + } + + }); + } + + @Override + public void fillRectangle(int x, int y, int width, int height) { + paintManager.performDrawFilled(paint -> surface.getCanvas() + .drawRect(RectangleConverter.createScaledRectangle(getScaler(), x, y, width, height), paint)); + } + + @Override + public void fillRoundRectangle(int x, int y, int width, int height, int arcWidth, int arcHeight) { + paintManager.performDrawFilled( + paint -> surface.getCanvas().drawRRect(RectangleConverter.createScaledRoundRectangle(getScaler(), x, y, + width, height, arcWidth / 2.0f, arcHeight / 2.0f), paint)); + } + + @Override + public Point textExtent(String text, int flags) { + return getScaler().scaleDown(resources.textExtent(text, flags)); + } + + @Override + public void setFont(org.eclipse.swt.graphics.Font font) { + this.resources.setFont(font); + } + + @Override + public void setClipping(int x, int y, int width, int height) { + setClipping(new Rectangle(x, y, width, height)); + } + + @Override + public void setTransform(Transform transform) { + + final var sc = getScaler(); + this.currentTransform = SkiaTransformConverter.toSkiaMatrix(transform, sc); + surface.getCanvas().setMatrix(this.currentTransform); + // Save the canvas state after applying the new transform, so subsequent + // operations (e.g. clipping) can be stacked and later restored independently + surface.getCanvas().save(); + + } + + @Override + public void setAlpha(int alpha) { + alpha = alpha & 0xFF; + this.alpha = alpha; + } + + @Override + public int getAlpha() { + return this.alpha; + } + + @Override + public void setLineWidth(int i) { + + if (i <= 0) { + this.lineWidth = 1; + } else { + this.lineWidth = i; + } + + } + + @Override + public int getAntialias() { + return antialias; + } + + @Override + public void setAntialias(int antialias) { + if (antialias != SWT.DEFAULT && antialias != SWT.ON && antialias != SWT.OFF) { + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + this.antialias = antialias; + } + + @Override + public void setAdvanced(boolean enable) { + // Nothing to do... + } + + @Override + public void setLineStyle(int lineStyle) { + if (lineStyle != SWT.LINE_SOLID && lineStyle != SWT.LINE_DASH && lineStyle != SWT.LINE_DOT + && lineStyle != SWT.LINE_DASHDOT && lineStyle != SWT.LINE_DASHDOTDOT && lineStyle != SWT.LINE_CUSTOM) { + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + this.lineStyle = lineStyle; + } + + @Override + public int getLineStyle() { + return lineStyle; + } + + @Override + public int getLineWidth() { + return lineWidth; + } + + @Override + public LineAttributes getLineAttributes() { + final LineAttributes attributes = getLineAttributesInPixels(); + attributes.width = getScaler().autoScaleDown(attributes.width); + if (attributes.dash != null) { + attributes.dash = getScaler().autoScaleDown(attributes.dash); + } + return attributes; + } + + LineAttributes getLineAttributesInPixels() { + return new LineAttributes(lineWidth, lineCap, lineJoin, lineStyle, lineDashes, dashOffset, miterLimit); + } + + @Override + public Rectangle getClipping() { + if (currentClipRegion != null) { + return currentClipBounds; + } + return canvas.getBounds(); + } + + @Override + public Point stringExtent(String string) { + return textExtent(string); + } + + @Override + public int getLineCap() { + return lineCap; + } + + @Override + public void copyArea(Image image, int x, int y) { + + if (image == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + + final io.github.humbleui.skija.Image skijaImage = SwtToSkiaImageConverter.convertSWTImageToSkijaImage(image, + getScaler().getNativeZoom(), resources); + try (final io.github.humbleui.skija.Image copiedArea = surface.makeImageSnapshot(RectangleConverter + .createScaledRectangle(getScaler(), x, y, skijaImage.getWidth(), skijaImage.getHeight()).toIRect())) { + + if (copiedArea == null) { + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + + try (final Surface imageSurface = surface.makeSurface(skijaImage.getWidth(), skijaImage.getHeight())) { + imageSurface.getCanvas().drawImage(copiedArea, 0, 0); + try (final io.github.humbleui.skija.Image snapshot = imageSurface.makeImageSnapshot()) { + final ImageData imgData = SkijaToSwtImageConverter.convertSkijaImageToImageData(snapshot); + Image i = null; + GC gc = null; + try { + i = new Image(device, imgData); + gc = new GC(image); + gc.drawImage(i, 0, 0); + } finally { + if (gc != null) { + gc.dispose(); + } + if (i != null) { + i.dispose(); + } + } + } + } + } + } + + @Override + public void copyArea(int srcX, int srcY, int width, int height, int destX, int destY) { + try (io.github.humbleui.skija.Image copiedArea = surface.makeImageSnapshot( + RectangleConverter.createScaledRectangle(getScaler(), srcX, srcY, width, height).toIRect())) { + surface.getCanvas().drawImage(copiedArea, getScaler().autoScaleUp(destX), getScaler().autoScaleUp(destY)); + } + } + + @Override + public void copyArea(int srcX, int srcY, int width, int height, int destX, int destY, boolean paint) { + + copyArea(srcX, srcY, width, height, destX, destY); + if (paint) { + // Save the canvas state before clipping + surface.getCanvas().save(); + // Clip to the destination rectangle so only this area is affected + surface.getCanvas() + .clipRect(RectangleConverter.createScaledRectangle(getScaler(), srcX, srcY, width, height)); + // Clear the clipped area with transparent background (simulates OS.SW_ERASE) + surface.getCanvas().clear(SkiaColorConverter.convertSWTColorToSkijaColor(getBackground())); + // Restore the canvas state + surface.getCanvas().restore(); + // Trigger redraw for the source area if using SWT Canvas + canvas.redraw(srcX, srcY, width, height, false); + } + } + + @Override + public boolean isClipped() { + return isClipSet; + } + + @Override + public int getFillRule() { + return fillRule; + } + + @Override + public void getClipping(Region region) { + if (region == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + if (region.isDisposed()) { + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + + if (this.currentClipBounds != null) { + + region.intersect(this.currentClipBounds); + region.add(this.currentClipBounds); + return; + } + + if (this.currentClipRegion != null) { + region.intersect(this.currentClipRegion); + region.add(this.currentClipRegion); + return; + } + } + + @Override + public int getAdvanceWidth(char ch) { + + final var f = getSkiaFont(); + final AtomicInteger result = new AtomicInteger(); + result.set(-1); + + paintManager.performDraw(paint -> { + paint.setAntiAlias(false); + paint.setMode(PaintMode.FILL); + + final var textWidth = f.measureTextWidth(String.valueOf(ch), paint); + result.set((int) Math.ceil(textWidth)); + + }); + + return result.get(); + + } + + @Override + public boolean getAdvanced() { + return true; + } + + @Override + public Pattern getBackgroundPattern() { + return backgroundPattern; + } + + @Override + public int getCharWidth(char ch) { + return getAdvanceWidth(ch); + } + + @Override + public Pattern getForegroundPattern() { + return foregroundPattern; + } + + @Override + public GCData getGCData() { + return null; + } + + @Override + public int getInterpolation() { + if (interpolationMode == SamplingMode.DEFAULT) { + return SWT.NONE; + } + if (interpolationMode == SamplingMode.LINEAR) { + return SWT.HIGH; + } + if (interpolationMode instanceof final FilterMipmap fm) { + if (fm.getFilterMode() == FilterMode.LINEAR && fm.getMipmapMode() == MipmapMode.LINEAR) { + return SWT.LOW; + } + } + return SWT.DEFAULT; + } + + @Override + public int[] getLineDash() { + if (lineDashes == null) { + return null; + } + final int[] lineDashesInt = new int[lineDashes.length]; + for (int i = 0; i < lineDashesInt.length; i++) { + lineDashesInt[i] = getScaler().autoScaleDownToInt(lineDashes[i]); + } + return lineDashesInt; + } + + @Override + public int getLineJoin() { + return lineJoin; + } + + @Override + public int getStyle() { + return style; + } + + @Override + public int getTextAntialias() { + return textAntiAlias; + } + + @Override + public void getTransform(Transform transform) { + if (transform == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + SkiaTransformConverter.fromSkiaMatrix(currentTransform, transform, getScaler()); + } + + @Override + public boolean getXORMode() { + return xorModeActive; + } + + @Override + public void setBackgroundPattern(Pattern pattern) { + if (pattern != null && pattern.isDisposed()) { + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + this.backgroundPattern = pattern; + } + + @Override + public void setClipping(Path path) { + final Canvas canvas = surface.getCanvas(); + if (isClipSet) { + canvas.restore(); // pop the previously saved clip layer + isClipSet = false; + } + if (path == null) { + return; + } + if (path.isDisposed()) { + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + try (final io.github.humbleui.skija.Path skijaPath = SkiaPathConverter.convertSWTPathToSkijaPath(path, + getScaler())) { + if (skijaPath == null) { + return; + } + skijaPath.setFillMode(fillRule == SWT.FILL_EVEN_ODD ? PathFillMode.EVEN_ODD : PathFillMode.WINDING); + // Push a new canvas layer so the clip can be undone later with restore() + canvas.save(); + isClipSet = true; + canvas.clipPath(skijaPath, ClipMode.INTERSECT, true); + } + } + + @Override + public void setClipping(Rectangle rect) { + + // Skija uses a canvas state stack; each save() pushes a new layer, + // and restore() pops it, removing the clipping region set in that layer. + // Since only one clip is tracked at a time, no explicit restore() is needed + // here + // because the clip will be cleared when the next setClipping call is made or on + // dispose. + final Canvas canvas = surface.getCanvas(); + if (isClipSet) { + canvas.restore(); // pop the previously saved clip layer + isClipSet = false; + } + if (rect == null) { + currentClipBounds = null; + return; + } + currentClipBounds = new Rectangle(rect.x, rect.y, rect.width, rect.height); + // Push a new canvas layer so the clip can be undone later with restore() + canvas.save(); + canvas.clipRect(RectangleConverter.createScaledRectangle(getScaler(), rect)); + isClipSet = true; + + } + + @Override + public void setClipping(Region region) { + + // Skija uses a canvas state stack; each save() pushes a new layer, + // and restore() pops it, removing the clipping region set in that layer. + final Canvas canvas = surface.getCanvas(); + if (isClipSet) { + canvas.restore(); // pop the previously saved clip layer + isClipSet = false; + } + currentClipBounds = null; + currentClipRegion = region; + + if (region == null) { + return; + } + + final SkiaRegionCalculator calc = new SkiaRegionCalculator(region, skiaExtension); + // Push a new canvas layer so the clip can be undone later with restore() + canvas.save(); + try (calc) { + canvas.clipRegion(calc.getSkiaRegion()); + } + isClipSet = true; + } + + @Override + public void setFillRule(int rule) { + if (rule != SWT.FILL_EVEN_ODD && rule != SWT.FILL_WINDING) { + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + this.fillRule = rule; + } + + @Override + public void setForegroundPattern(Pattern pattern) { + if (pattern != null && pattern.isDisposed()) { + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + this.foregroundPattern = pattern; + } + + @Override + public void setInterpolation(int interpolation) { + + // GDI | Skia | description + // NearestNeighbor SKFilterMode.Nearest hart pixels + // Low / Bilinear SKFilterMode.Linear simple linear + // High / Bicubic SKCubicResampler.Mitchell high quality + // HighQualityBicubic SKCubicResampler.CatmullRom maximum sharp, cubic + // interpolation + + switch (interpolation) { + case SWT.NONE -> this.interpolationMode = SamplingMode.DEFAULT; // Nearest neighbor + case SWT.LOW -> this.interpolationMode = SamplingMode.LINEAR; + case SWT.DEFAULT -> this.interpolationMode = SamplingMode.MITCHELL; + case SWT.HIGH -> this.interpolationMode = SamplingMode.CATMULL_ROM; + default -> SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + } + + @Override + public void setLineAttributes(LineAttributes attributes) { + if (attributes == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + + if(attributes.width <= 0) { + attributes.width = 1; + } + + final float scaledWidth = getScaler().autoScaleUp(attributes.width); + setLineAttributesInPixels(attributes, scaledWidth); + } + + private void setLineAttributesInPixels(LineAttributes attributes, float scaledWidth) { + if (isDisposed()) { + SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); + } + boolean changed = false; + if (scaledWidth != this.lineWidth) { + this.lineWidth = (int) Math.ceil(scaledWidth); + changed = true; + } + // Handle line style with validation + int lineStyle = attributes.style; + if (lineStyle != this.lineStyle) { + switch (lineStyle) { + case SWT.NONE: + lineStyle = SWT.LINE_SOLID; + break; + case SWT.LINE_SOLID: + case SWT.LINE_DASH: + case SWT.LINE_DOT: + case SWT.LINE_DASHDOT: + case SWT.LINE_DASHDOTDOT: + break; + case SWT.LINE_CUSTOM: + if (attributes.dash == null) { + lineStyle = SWT.LINE_SOLID; + } + break; + default: + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + this.lineStyle = lineStyle; + changed = true; + } + // Handle line cap with validation + final int cap = attributes.cap; + if (cap != this.lineCap) { + switch (cap) { + case SWT.CAP_FLAT: + case SWT.CAP_ROUND: + case SWT.CAP_SQUARE: + break; + default: + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + this.lineCap = cap; + changed = true; + } + // Handle line join with validation + final int join = attributes.join; + if (join != this.lineJoin) { + switch (join) { + case SWT.JOIN_MITER: + case SWT.JOIN_ROUND: + case SWT.JOIN_BEVEL: + break; + default: + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + this.lineJoin = join; + changed = true; + } + // Handle dash pattern with validation and DPI scaling + final float[] dashes = attributes.dash; + final float[] currentDashes = this.lineDashes; + + if (dashes != null && dashes.length > 0) { + boolean dashesChanged = currentDashes == null || currentDashes.length != dashes.length; + final float[] newDashes = new float[dashes.length]; + + for (int i = 0; i < dashes.length; i++) { + final float dash = dashes[i]; + if (dash <= 0) { + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + + // Scale dash values for DPI + newDashes[i] = getScaler().autoScaleUp(dash); + + if (!dashesChanged && currentDashes != null && currentDashes[i] != newDashes[i]) { + dashesChanged = true; + } + } + if (dashesChanged) { + this.lineDashes = newDashes; + changed = true; + } + } else { + // Clear dash pattern + if (currentDashes != null && currentDashes.length > 0) { + this.lineDashes = null; + changed = true; + } + } + // Handle dash offset - store for use in createPathEffectForLineStyle() + final float dashOffset = attributes.dashOffset; + if (this.dashOffset != dashOffset) { + this.dashOffset = dashOffset; + changed = true; + } + // Handle miter limit - store for use in performDraw() + final float miterLimit = attributes.miterLimit; + if (this.miterLimit != miterLimit) { + this.miterLimit = miterLimit; + changed = true; + } + if (!changed) { + return; + } + } + + @Override + public void setLineCap(int cap) { + if (cap != SWT.CAP_FLAT && cap != SWT.CAP_ROUND && cap != SWT.CAP_SQUARE) { + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + this.lineCap = cap; + } + + @Override + public void setLineDash(int[] dashes) { + if (dashes != null && dashes.length > 0) { + boolean changed = this.lineStyle != SWT.LINE_CUSTOM || lineDashes == null + || lineDashes.length != dashes.length; + final float[] newDashes = new float[dashes.length]; + for (int i = 0; i < dashes.length; i++) { + if (dashes[i] <= 0) { + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + newDashes[i] = getScaler().autoScaleUp(dashes[i]); + if (!changed && lineDashes != null && lineDashes[i] != newDashes[i]) { + changed = true; + } + } + if (!changed) { + return; + } + this.lineDashes = newDashes; + this.lineStyle = SWT.LINE_CUSTOM; + } else { + if (this.lineStyle == SWT.LINE_SOLID && (lineDashes == null || lineDashes.length == 0)) { + return; + } + this.lineDashes = null; + this.lineStyle = SWT.LINE_SOLID; + } + } + + @Override + public void setLineJoin(int join) { + if (join != SWT.JOIN_MITER && join != SWT.JOIN_ROUND && join != SWT.JOIN_BEVEL) { + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + this.lineJoin = join; + } + + @Override + public void setXORMode(boolean xor) { + this.xorModeActive = xor; + } + + @Override + public void setTextAntialias(int antialias) { + this.textAntiAlias = antialias; + } + + @Override + public Color getBackground() { + return resources.getBackground(); + } + + @Override + public Device getDevice() { + return device; + } + + @Override + public org.eclipse.swt.graphics.Font getFont() { + return this.resources.getFont(); + } + + @Override + public boolean isDisposed() { + return surface.isClosed(); + } + + @Override + public FontMetrics getFontMetrics() { + + final var font = getSkiaFont(); + final var m = font.getMetrics(); + final var fe = new FontMetricsExtension(new SkiaFontMetrics(m, getScaler())); + return fe; + } + + @Override + public Drawable getDrawable() { + return canvas; + } + + @Override + public void textLayoutDraw(TextLayout textLayout, GC gc, int xInPoints, int yInPoints, int selectionStart, + int selectionEnd, Color selectionForeground, Color selectionBackground, int flags) { + + final var rectangle = textLayout.getBounds(); + + if (rectangle.width <= 0 || rectangle.height <= 0) { + return; + } + + Image i = null; + GC nativeGC = null; + try { + i = new Image(device, rectangle.width, rectangle.height); + nativeGC = new GC(i); + + textLayout.draw(nativeGC, 0, 0, selectionStart, selectionEnd, selectionForeground, selectionBackground, + flags); + + drawImage(i, rectangle.x, rectangle.y, rectangle.width, rectangle.height, xInPoints, yInPoints, + rectangle.width, rectangle.height); + } finally { + if (nativeGC != null) { + nativeGC.dispose(); + } + if (i != null) { + i.dispose(); + } + } + + } + + SkiaResources getSkiaResources() { + return this.resources; + } + + Surface getSkiaSurface() { + return this.surface; + } + + ISkiaCanvasExtension getSkiaExtension() { + return this.skiaExtension; + } + + float[] getLineDashes() { + return this.lineDashes; + } + +} diff --git a/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkiaPaintManager.java b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkiaPaintManager.java new file mode 100644 index 00000000000..8c470233619 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkiaPaintManager.java @@ -0,0 +1,187 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * Contributors: + * SAP SE and others - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.internal.graphics; + +import java.util.function.Consumer; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Pattern; +import org.eclipse.swt.internal.skia.DpiScalerUtil; + +import io.github.humbleui.skija.Paint; +import io.github.humbleui.skija.PaintMode; +import io.github.humbleui.skija.PaintStrokeCap; +import io.github.humbleui.skija.PaintStrokeJoin; +import io.github.humbleui.skija.PathEffect; +import io.github.humbleui.skija.Shader; + +public class SkiaPaintManager { + + // Skia format: dash/dot widths and whitespace widths must be considered. + static final float[] LINE_DOT_ZERO = new float[] { 1, 1 }; + static final float[] LINE_DASH_ZERO = new float[] { 3, 1 }; + static final float[] LINE_DASHDOT_ZERO = new float[] { 3, 1, 1, 1 }; + static final float[] LINE_DASHDOTDOT_ZERO = new float[] { 3, 1, 1, 1, 1, 1 }; + + private final SkiaGC gc; + + public SkiaPaintManager(SkiaGC gc) { + this.gc = gc; + } + + public void performDraw(Consumer operations) { + try (final Paint paint = new Paint()) { + paint.setAlpha(gc.getAlpha()); + paint.setColor(SkiaColorConverter.convertSWTColorToSkijaColor(gc.getForeground(), gc.getAlpha())); + paint.setAntiAlias(gc.getAntialias() != SWT.OFF); + if (gc.getXORMode()) { + paint.setBlendMode(io.github.humbleui.skija.BlendMode.DIFFERENCE); + } else { + paint.setBlendMode(io.github.humbleui.skija.BlendMode.SRC_OVER); + } + final var scaledLineWidth = gc.getScaler().autoScaleUp(gc.getLineWidth() * 1F); + paint.setMode(PaintMode.STROKE); + paint.setStrokeWidth(scaledLineWidth); + paint.setStrokeCap(getSkijaLineCap(gc.getLineCap())); + paint.setStrokeJoin(getSkijaLineJoin(gc.getLineJoin())); + + try (var pathEffect = createPathEffect(scaledLineWidth, gc.getScaler())) { + + paint.setPathEffect(pathEffect); + + if (gc.getForegroundPattern() != null && !gc.getForegroundPattern().isDisposed()) { + try (Shader shader = convertSWTPatternToSkijaShader(gc.getForegroundPattern())) { + if (shader != null) { + paint.setShader(shader); + } + operations.accept(paint); + } + } else { + operations.accept(paint); + } + } + } + } + + private PathEffect createPathEffect(float scaledLineWidth, DpiScalerUtil scaler) { + + final var floats = switch (gc.getLineStyle()) { + case SWT.LINE_SOLID -> null; + case SWT.LINE_DOT -> LINE_DOT_ZERO; + case SWT.LINE_DASH -> LINE_DASH_ZERO; + case SWT.LINE_DASHDOT -> LINE_DASHDOT_ZERO; + case SWT.LINE_DASHDOTDOT -> LINE_DASHDOTDOT_ZERO; + case SWT.LINE_CUSTOM -> gc.getLineDashes(); + default -> null; + }; + + if (floats == null || floats.length == 0) { + return null; + } + + if (gc.getLineStyle() == SWT.LINE_CUSTOM) { + if (floats.length % 2 != 0) { + // Skia requires even number of elements in the array, so we duplicate the + // pattern to make it even. + final float[] newFloats = new float[floats.length * 2]; + System.arraycopy(floats, 0, newFloats, 0, floats.length); + System.arraycopy(floats, 0, newFloats, floats.length, floats.length); + return PathEffect.makeDash(getScaledPathFloats(scaler.autoScaleUp(1), newFloats, scaler), 0f); + } + + } + + final var scaledFloats = getScaledPathFloats(scaledLineWidth, floats, scaler); + if(scaledFloats == null || scaledFloats.length == 0) { + return null; + } + + return PathEffect.makeDash(scaledFloats, 0f); + + } + + private static float[] getScaledPathFloats(float scaledLineWidth, float[] lineDotZero, DpiScalerUtil scaler) { + final float[] result = new float[lineDotZero.length]; + for (int i = 0; i < lineDotZero.length; i++) { + result[i] = lineDotZero[i] * scaledLineWidth; + } + return result; + } + + public void performDrawFilled(Consumer operations) { + try (final Paint paint = new Paint()) { + paint.setMode(PaintMode.FILL); + paint.setColor(SkiaColorConverter.convertSWTColorToSkijaColor(gc.getBackground(), gc.getAlpha())); + paint.setAntiAlias(gc.getAntialias() != SWT.OFF); + if (gc.getXORMode()) { + paint.setBlendMode(io.github.humbleui.skija.BlendMode.DIFFERENCE); + } else { + paint.setBlendMode(io.github.humbleui.skija.BlendMode.SRC_OVER); + } + if (gc.getBackgroundPattern() != null && !gc.getBackgroundPattern().isDisposed()) { + try (Shader shader = convertSWTPatternToSkijaShader(gc.getBackgroundPattern())) { + if (shader != null) { + paint.setShader(shader); + } + operations.accept(paint); + } + } else { + operations.accept(paint); + } + } + } + + public static PaintStrokeCap getSkijaLineCap(int lineCap) { + if (lineCap == SWT.CAP_SQUARE) { + return PaintStrokeCap.SQUARE; + } + if (lineCap == SWT.CAP_ROUND) { + return PaintStrokeCap.ROUND; + } + return PaintStrokeCap.BUTT; + } + + public static PaintStrokeJoin getSkijaLineJoin(int lineJoin) { + if (lineJoin == SWT.JOIN_ROUND) { + return PaintStrokeJoin.ROUND; + } + if (lineJoin == SWT.JOIN_BEVEL) { + return PaintStrokeJoin.BEVEL; + } + return PaintStrokeJoin.MITER; + } + + /** + * Converts an SWT Pattern to a Skija Shader. + * + * @param pattern the SWT Pattern to convert + * @return the Skija Shader or null if conversion fails + */ + public Shader convertSWTPatternToSkijaShader(Pattern pattern) { + final var props = org.eclipse.swt.graphics.PatternProperties.get(pattern); + final var sc = gc.getScaler(); + if (props.getImage() == null) { + final int col1 = SkiaColorConverter.convertSWTColorToSkijaColor(props.getColor1(), props.getAlpha1()); + final int col2 = SkiaColorConverter.convertSWTColorToSkijaColor(props.getColor2(), props.getAlpha2()); + final var gs = new io.github.humbleui.skija.GradientStyle(io.github.humbleui.skija.FilterTileMode.REPEAT, + true, null); + return Shader.makeLinearGradient(sc.autoScaleUp(props.getBaseX1()), sc.autoScaleUp(props.getBaseY1()), + sc.autoScaleUp(props.getBaseX2()), sc.autoScaleUp(props.getBaseY2()), new int[] { col1, col2 }, + null, gs); + } + try (final var image = SwtToSkiaImageConverter.convertSWTImageToSkijaImage(props.getImage(), + gc.getScaler().getNativeZoom(), gc.getSkiaResources())) { + return image.makeShader(io.github.humbleui.skija.FilterTileMode.REPEAT); + } + } +} \ No newline at end of file diff --git a/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkiaPathConverter.java b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkiaPathConverter.java new file mode 100644 index 00000000000..bd7e5529ea6 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkiaPathConverter.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * Contributors: + * SAP SE and others - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.internal.graphics; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Path; +import org.eclipse.swt.graphics.PathData; +import org.eclipse.swt.internal.skia.DpiScalerUtil; + +import io.github.humbleui.skija.PathFillMode; + +public class SkiaPathConverter { + + public static io.github.humbleui.skija.Path convertSWTPathToSkijaPath(Path swtPath, DpiScalerUtil sc) { + + if (swtPath == null || swtPath.isDisposed()) { + return null; + } + final PathData data = swtPath.getPathData(); + + try (final io.github.humbleui.skija.PathBuilder skijaPathBuilder = new io.github.humbleui.skija.PathBuilder()) { + + final float[] pts = data.points; + final byte[] types = data.types; + int pi = 0; + for (final byte type : types) { + switch (type) { + case SWT.PATH_MOVE_TO: + skijaPathBuilder.moveTo(sc.autoScaleUp(pts[pi++]), sc.autoScaleUp(pts[pi++])); + break; + case SWT.PATH_LINE_TO: + skijaPathBuilder.lineTo(sc.autoScaleUp(pts[pi++]), sc.autoScaleUp(pts[pi++])); + break; + case SWT.PATH_CUBIC_TO: + skijaPathBuilder.cubicTo(sc.autoScaleUp(pts[pi++]), sc.autoScaleUp(pts[pi++]), + sc.autoScaleUp(pts[pi++]), sc.autoScaleUp(pts[pi++]), sc.autoScaleUp(pts[pi++]), + sc.autoScaleUp(pts[pi++])); + break; + case SWT.PATH_QUAD_TO: + skijaPathBuilder.quadTo(sc.autoScaleUp(pts[pi++]), sc.autoScaleUp(pts[pi++]), + sc.autoScaleUp(pts[pi++]), sc.autoScaleUp(pts[pi++])); + break; + case SWT.PATH_CLOSE: + skijaPathBuilder.closePath(); + break; + default: + } + } + final var p = skijaPathBuilder.build(); + return p; + } + + } + + public static io.github.humbleui.skija.Path createPath(int[] pointArray,int fillRule , DpiScalerUtil sc) { + // Create Skija path for the polygon + try (io.github.humbleui.skija.PathBuilder pathBuilder = new io.github.humbleui.skija.PathBuilder()) { // Move + // to + // first + // point + pathBuilder.moveTo(sc.autoScaleUp(pointArray[0]), sc.autoScaleUp(pointArray[1])); + // Add lines to subsequent points + for (int i = 2; i < pointArray.length; i += 2) { + pathBuilder.lineTo(sc.autoScaleUp(pointArray[i]), sc.autoScaleUp(pointArray[i + 1])); + } + // Close the path to form a polygon + pathBuilder.closePath(); + pathBuilder.setFillMode(fillRule == SWT.FILL_EVEN_ODD ? PathFillMode.EVEN_ODD : PathFillMode.WINDING); + // Fill the polygon + return pathBuilder.build(); + } + } + +} diff --git a/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkiaRegionCalculator.java b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkiaRegionCalculator.java new file mode 100644 index 00000000000..b2ad2c8ef74 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkiaRegionCalculator.java @@ -0,0 +1,178 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.swt.internal.graphics; + +import java.util.HashMap; + +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.graphics.Region; +import org.eclipse.swt.graphics.RegionLog; +import org.eclipse.swt.graphics.RegionLog.OpType; +import org.eclipse.swt.graphics.RegionLog.Operation; +import org.eclipse.swt.internal.skia.DpiScalerUtil; +import org.eclipse.swt.internal.skia.ISkiaCanvasExtension; + +import io.github.humbleui.skija.RegionOp; +import io.github.humbleui.types.IRect; + +public class SkiaRegionCalculator implements AutoCloseable { + + private final static HashMap operationMapping = new HashMap(); + + static { + operationMapping.put(OpType.ADD, RegionOp.UNION); + operationMapping.put(OpType.SUBTRACT, RegionOp.DIFFERENCE); + operationMapping.put(OpType.INTERSECT, RegionOp.INTERSECT); + } + + private final org.eclipse.swt.graphics.Region region; + private io.github.humbleui.skija.Region calculatedRegion; + private final ISkiaCanvasExtension skiaExtension; + + public SkiaRegionCalculator(org.eclipse.swt.graphics.Region r, ISkiaCanvasExtension skiaExtension) { + this.region = r; + this.skiaExtension = skiaExtension; + } + + io.github.humbleui.skija.Region getSkiaRegion() { + + // just all operations for the region creation will be executed to get the skia + // region. + + if (calculatedRegion != null) { + return calculatedRegion; + } + + final io.github.humbleui.skija.Region reg = new io.github.humbleui.skija.Region(); + final var log = RegionLog.getLog(region); + for (final var o : log.getOperations()) { + apply(o, reg); + } + + calculatedRegion = reg; + return calculatedRegion; + + } + + @Override + public void close() { + if (calculatedRegion != null) { + calculatedRegion.close(); + calculatedRegion = null; + } + } + + private void apply(Operation o, io.github.humbleui.skija.Region reg) { + final RegionOp skiaOperation = operationMapping.get(o.type()); + if (skiaOperation != null) { + executeOperation(skiaOperation, o.executionObject(), reg); + return; + } + + if (OpType.TRANSLATE.equals(o.type())) { + if (o.executionObject() instanceof final Point p) { + reg.translate(p.x, p.y); + return; + } + } + + throw new IllegalStateException("Unknown type and object: " + o); //$NON-NLS-1$ + + } + + private void executeOperation(RegionOp skiaOperation, Object ob, io.github.humbleui.skija.Region reg) { + if (ob instanceof final int[] polygon) { + final var tempReg = createPolygonSkiaRegion(polygon); + reg.op(tempReg, skiaOperation); + } else if (ob instanceof final Rectangle rec) { + final var irect = createRectRegion(rec); + reg.op(irect, skiaOperation); + } else if (ob instanceof final Region otherReg) { + try (final SkiaRegionCalculator src = new SkiaRegionCalculator(otherReg, skiaExtension)) { + reg.op(src.getSkiaRegion(), skiaOperation); + } + } + } + + /** + * Creates a Skija region from an SWT rectangle by converting it to a polygon. + */ + private io.github.humbleui.skija.Region createRectRegion(Rectangle rec) { + final var rect = new IRect(rec.x, rec.y, rec.x + rec.width, rec.y + rec.height); + + return createPolygonSkiaRegion(new int[] { rect.getLeft(), rect.getTop(), rect.getRight(), rect.getTop(), + rect.getRight(), rect.getBottom(), rect.getLeft(), rect.getBottom() }); + + } + + private io.github.humbleui.skija.Region createPolygonSkiaRegion(int[] polygon) { + + final io.github.humbleui.skija.Region r = new io.github.humbleui.skija.Region(); + + final var scaler = new DpiScalerUtil(skiaExtension.getScaler()); + + try (final var pathBuilder = new io.github.humbleui.skija.PathBuilder()) { + pathBuilder.addPolygon(scaler.autoScaleUp(toFloat(polygon)), true); + + final Point maxV = getMax(scaler.autoScaleUp(polygon)); + + r.setRect(new IRect(0, 0, maxV.x, maxV.y)); + + // a path has to be set for a clipping region. + // so first we have to create a region big enough for the path and then set the + // path. + try (var path = pathBuilder.build()) { + r.setPath(path, r); + } + + return r; + } + } + + /** + * + * max value on x and on y in one point. + * + * @param polygon + * @return + */ + private static Point getMax(int[] polygon) { + + final var p = new Point(0, 0); + + for (int i = 0; i < polygon.length; i++) { + + if (i % 2 == 0) { + p.x = Math.max(polygon[i], p.x); + } + if (i % 2 == 1) { + p.y = Math.max(polygon[i], p.y); + } + + } + + return p; + } + + private static float[] toFloat(int[] arr) { + + final float[] r = new float[arr.length]; + + for (int i = 0; i < arr.length; i++) { + r[i] = arr[i]; + } + + return r; + + } + +} \ No newline at end of file diff --git a/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkiaTextDrawing.java b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkiaTextDrawing.java new file mode 100644 index 00000000000..38bcbeba3a4 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkiaTextDrawing.java @@ -0,0 +1,239 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * Contributors: + * SAP SE and others - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.internal.graphics; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.internal.canvasext.FontProperties; +import org.eclipse.swt.internal.canvasext.Logger; +import org.eclipse.swt.internal.skia.DpiScalerUtil; +import org.eclipse.swt.internal.skia.SkiaResources; + +import io.github.humbleui.skija.BlendMode; +import io.github.humbleui.skija.FontEdging; +import io.github.humbleui.skija.Paint; +import io.github.humbleui.skija.PaintMode; +import io.github.humbleui.skija.PaintStrokeCap; +import io.github.humbleui.skija.Surface; +import io.github.humbleui.types.Rect; + +public class SkiaTextDrawing { + // only for debug purpose use text drawing without cache. + + public static void drawText(SkiaGC gc, String[] splits, int flags, int x, int y) { + + if (SkiaResources.USE_TEXT_IMAGE_CACHE) { + drawTextBlobWithCache(gc, splits, flags, x, y); + return; + } + drawTextBlobNoCache(gc, splits, flags, x, y); + + } + + private static boolean isTransparent(int flags) { + return (SWT.DRAW_TRANSPARENT & flags) != 0; + } + + private static void drawTextBlobNoCache(SkiaGC gc, String[] splits, int flags, int x, int y) { + + final var paintManager = gc.getPaintManager(); + final var resources = gc.getSkiaResources(); + final var surface = gc.getSkiaSurface(); + + final var scaler = new DpiScalerUtil(resources.getScaler()); + + final boolean transparent = isTransparent(flags); + final boolean antiAlias = gc.getAntialias() != SWT.OFF || gc.getTextAntialias() != SWT.OFF; + final int[] yPos = new int[1]; + yPos[0] = y; + final var f = gc.getSkiaResources().getSkiaFont(); + + paintManager.performDraw(fgp -> { + fgp.setAntiAlias(antiAlias); + fgp.setMode(PaintMode.FILL); + f.setSubpixel(antiAlias); + f.setEdging(antiAlias ? FontEdging.SUBPIXEL_ANTI_ALIAS : FontEdging.ALIAS); + + fgp.setStrokeWidth(1); + fgp.setStrokeCap(PaintStrokeCap.BUTT); + fgp.setPathEffect(null); + for (final var text : splits) { + + final var metric = f.getMetrics(); + final var asc = metric.getAscent(); + final var des = metric.getDescent(); + final var leading = metric.getLeading(); + + final var ascI = (int) Math.ceil(Math.abs(asc)); + final var desI = (int) Math.ceil(Math.abs(des)); + final var heightI = ascI + desI; + final var r = scaler.scaleSize(x, yPos[0]); + + if (!transparent) { + + // draw rectangle background for the text. + + // Skia draws a text with a slightly right shift. So textExtent is insufficient + // in the width to make sure, the background area is big enough + // So make the area a little bit wider. + + // The background rectangle can be a little bit smaller, and in the worst + // case a small part of the text is not covered. + // Similar in windows. Actually for some fonts and sizes this solution is + // already better than windows. + + // heuristic number. After 0.12 of the ascent, the text is usually sufficiently + // in the rectangle area. + final double endOfRectangle = Math.abs(asc) * 0.12; + final var rect = f.measureText(text, fgp); + + final Point size = new Point((int) Math.ceil(rect.getWidth() + endOfRectangle), + (int) Math.ceil(heightI + leading)); + + paintManager.performDrawFilled(paint -> { + surface.getCanvas().drawRect(new Rect(r.x, r.y, r.x + size.x, r.y + size.y), paint); + }); + + } + surface.getCanvas().drawString(text, r.x, r.y + ascI, f, fgp); + yPos[0] += heightI + leading; + } + }); + + } + + private static void drawTextBlobWithCache(SkiaGC gc, String[] splits, int flags, int xIn, int yIn) { + + final var paintManager = gc.getPaintManager(); + final var resources = gc.getSkiaResources(); + final var surface = gc.getSkiaSurface(); + final var scaler = new DpiScalerUtil(resources.getScaler()); + + final boolean transparent = isTransparent(flags); + + final int x = scaler.autoScaleUp(xIn); + final int y = scaler.autoScaleUp(yIn); + + final var f = resources.getSkiaFont(); + final FontProperties props = FontProperties.getFontProperties(gc.getFont()); + final int backgroundColor = gc.getAlpha() < 255 + ? SkiaColorConverter.convertSWTColorToSkijaColor(gc.getBackground(), gc.getAlpha()) + : SkiaColorConverter.convertSWTColorToSkijaColor(gc.getBackground()); + final int foregroundColor = SkiaColorConverter.convertSWTColorToSkijaColor(gc.getForeground(), gc.getAlpha()); + final boolean antiAlias = gc.getAntialias() != SWT.OFF || gc.getTextAntialias() != SWT.OFF; + + final int[] yPos = new int[1]; + yPos[0] = y; + for (final var text : splits) { + + final var cachedImage = resources.getTextImage(text, props, transparent, backgroundColor, foregroundColor, + antiAlias); + if (cachedImage != null && !cachedImage.isClosed()) { + surface.getCanvas().drawImage(cachedImage, x, yPos[0]); + yPos[0] += Math.ceil(cachedImage.getHeight()); + continue; + } + paintManager.performDraw(fgp -> { + + fgp.setAntiAlias(antiAlias); + fgp.setMode(PaintMode.FILL); + f.setSubpixel(antiAlias); + f.setEdging(antiAlias ? FontEdging.SUBPIXEL_ANTI_ALIAS : FontEdging.ALIAS); + + fgp.setStrokeWidth(1); + fgp.setStrokeCap(PaintStrokeCap.BUTT); + fgp.setPathEffect(null); + fgp.setBlendMode(BlendMode.SRC_IN); + + final var rect = f.measureText(text, fgp); + final var metric = f.getMetrics(); + final var asc = metric.getAscent(); + final var des = metric.getDescent(); + final var leading = metric.getLeading(); + + final var ascI = (int) Math.ceil(Math.abs(asc)); + final var desI = (int) Math.ceil(Math.abs(des)); + final var heightI = ascI + desI; + + // skija draws a text with a slightly right shift. So textExtent is insufficient + // in + // the width. So make the background area a little bit wider. + + // Idea: + // 1. the support surface should always be wide enough to contain the complete + // text. + // 2. the background rectangle can be a little bit smaller, and in the worst + // case a small part of the text is not covered. + // Similar in windows. Actually for some fonts and sizes this solution is + // already better than windows. + + final double additionalArea = Math.abs(asc) * 2; + + final Point size = new Point((int) Math.ceil(rect.getWidth() + additionalArea), + (int) Math.ceil(heightI + leading)); + + final int MAX_SURFACE_WIDTH = 8192; // Documented practical limit + int surfaceWidth = size.x; + if (surfaceWidth > MAX_SURFACE_WIDTH) { + Logger.logException(new IllegalStateException("Surface width restricted: calculated=" + surfaceWidth //$NON-NLS-1$ + + ", max=" + MAX_SURFACE_WIDTH + ", font=" + props.name + ", size=" + size //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + + ", backgroundColor=" + backgroundColor + ", foregroundColor=" + foregroundColor //$NON-NLS-1$//$NON-NLS-2$ + + ", antiAlias=" + antiAlias + ", transparent=" + transparent + ", text='" + text + "'")); //$NON-NLS-1$//$NON-NLS-2$ //$NON-NLS-3$//$NON-NLS-4$ + surfaceWidth = MAX_SURFACE_WIDTH; + } + if (surfaceWidth <= 0 || size.y <= 0) { + final StringBuilder sb = new StringBuilder(); + sb.append("Calculated text image size is invalid. font=").append(props.name).append(", size=") //$NON-NLS-1$ //$NON-NLS-2$ + .append(size).append(", text='").append(text).append("'"); //$NON-NLS-1$ //$NON-NLS-2$ + Logger.logException(new IllegalStateException(sb.toString())); + return; + } + try (Surface supportSurface = gc.getSkiaExtension().createSupportSurface(surfaceWidth, size.y)) { + + supportSurface.getCanvas().clear(0); + if (!transparent) { + + // heuristic number. After 0.12 of the ascent, the text is usually sufficiently + // in the rectangle area. + final double endOfRectangle = Math.abs(asc) * 0.12; + // always clear the support surface, then fill a specific rectangle area with + // the background color, the rest stays transparent. + try (Paint p = new Paint()) { + p.setColor( + SkiaColorConverter.convertSWTColorToSkijaColor(gc.getBackground(), gc.getAlpha())); + p.setMode(PaintMode.FILL); + supportSurface.getCanvas().drawRect( + new Rect(0, 0, (int) Math.ceil(rect.getWidth() + endOfRectangle), size.y), p); + } + + } else { + // very important at text on transparent background. Blend over the source, + // otherwise the text won't be visible. + fgp.setBlendMode(BlendMode.SRC_OVER); + } + + supportSurface.getCanvas().drawString(text, 0, ascI, f, fgp); + final var image = supportSurface.makeImageSnapshot(); + resources.cacheTextImage(text, props, transparent, backgroundColor, foregroundColor, antiAlias, + image); + surface.getCanvas().drawImage(image, x, yPos[0]); + yPos[0] += Math.ceil(image.getHeight()); + + } + + }); + } + + } + +} diff --git a/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkiaTransformConverter.java b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkiaTransformConverter.java new file mode 100644 index 00000000000..01b6351b45f --- /dev/null +++ b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkiaTransformConverter.java @@ -0,0 +1,60 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * Contributors: + * SAP SE and others - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.internal.graphics; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Transform; +import org.eclipse.swt.internal.skia.DpiScalerUtil; + +import io.github.humbleui.skija.Matrix33; + +public class SkiaTransformConverter { + + public static Matrix33 toSkiaMatrix(Transform transform, DpiScalerUtil sc) { + + if (transform == null) { + return Matrix33.IDENTITY; + } + if (transform.isDisposed()) { + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + final float[] elements = new float[6]; + transform.getElements(elements); + // SWT Transform: [m11, m12, m21, m22, dx, dy] + // Skia Matrix33: [scaleX, skewX, transX, skewY, scaleY, transY, persp0, + // persp1, persp2] + // Correct mapping: SWT [0,1,2,3,4,5] -> Skija [0,2,4,1,3,5,0,0,1] + final float[] skijaMat = new float[] { elements[0], // m11 -> scaleX + elements[2], // m21 -> skewX + sc.autoScaleUp(elements[4]), // dx -> transX + elements[1], // m12 -> skewY + elements[3], // m22 -> scaleY + sc.autoScaleUp(elements[5]), // dy -> transY + 0, 0, 1 // perspective elements + }; + return new Matrix33(skijaMat); + + } + + public static void fromSkiaMatrix(Matrix33 skiaMatrix, Transform transform, DpiScalerUtil sc) { + + final float[] m = skiaMatrix.getMat(); + // Skija Matrix33: [scaleX, skewX, transX, skewY, scaleY, transY, persp0, + // persp1, persp2] + // SWT Transform: [m11, m12, m21, m22, dx, dy] + // Correct inverse mapping: Skija [0,1,2,3,4,5] -> SWT [0,3,1,4,2,5] + transform.setElements(m[0], m[3], sc.autoScaleDown(m[1]), m[4], m[2], sc.autoScaleDown(m[5])); + + } + +} diff --git a/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkijaToSwtImageConverter.java b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkijaToSwtImageConverter.java new file mode 100644 index 00000000000..a6e03419057 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SkijaToSwtImageConverter.java @@ -0,0 +1,114 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * Contributors: + * SAP SE and others - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.internal.graphics; + +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.PaletteData; +import org.eclipse.swt.graphics.RGB; + +import io.github.humbleui.skija.Bitmap; +import io.github.humbleui.skija.ColorType; + +public class SkijaToSwtImageConverter { + public static ImageData convertSkijaImageToImageData(io.github.humbleui.skija.Image image) { + try (final Bitmap bm = Bitmap.makeFromImage(image)) { + final var colType = bm.getColorType(); + final byte[] alphas = new byte[bm.getHeight() * bm.getWidth()]; + final var source = bm.readPixels(); + final byte[] convertedData = new byte[bm.getHeight() * bm.getWidth() * 3]; + final var colorOrder = getPixelOrder(colType); + for (int y = 0; y < bm.getHeight(); y++) { + for (int x = 0; x < bm.getWidth(); x++) { + byte alpha = convertAlphaTo255Range(bm.getAlphaf(x, y)); + final int index = (x + y * bm.getWidth()) * 4; + final byte red = source[index + colorOrder[0]]; + final byte green = source[index + colorOrder[1]]; + final byte blue = source[index + colorOrder[2]]; + alpha = source[index + colorOrder[3]]; + alphas[x + y * bm.getWidth()] = alpha; + final int target = (x + y * bm.getWidth()) * 3; + convertedData[target + 0] = (red); + convertedData[target + 1] = (green); + convertedData[target + 2] = (blue); + } + } + final ImageData d = new ImageData(bm.getWidth(), bm.getHeight(), 24, getPaletteData(colType)); + d.data = convertedData; + d.alphaData = alphas; + d.bytesPerLine = d.width * 3; + return d; + } + } + + public static byte convertAlphaTo255Range(float alphaF) { + if (alphaF < 0.0f) { + alphaF = 0.0f; + } + if (alphaF > 1.0f) { + alphaF = 1.0f; + } + return (byte) Math.round(alphaF * 255); + } + + public static int[] getPixelOrder(ColorType colorType) { + return switch (colorType) { + case ALPHA_8 -> new int[] { 0 }; + case RGB_565 -> new int[] { 0, 1, 2 }; + case ARGB_4444 -> new int[] { 1, 2, 3, 0 }; + case RGBA_8888 -> new int[] { 0, 1, 2, 3 }; + case RGB_888X -> new int[] { 0, 1, 2, 3 }; + case BGRA_8888 -> new int[] { 2, 1, 0, 3 }; + case RGBA_1010102 -> new int[] { 0, 1, 2, 3 }; + case BGRA_1010102 -> new int[] { 2, 1, 0, 3 }; + case RGB_101010X -> new int[] { 0, 1, 2, 3 }; + case BGR_101010X -> new int[] { 2, 1, 0, 3 }; + case RGBA_F16NORM -> new int[] { 0, 1, 2, 3 }; + case RGBA_F16 -> new int[] { 0, 1, 2, 3 }; + case RGBA_F32 -> new int[] { 0, 1, 2, 3 }; + case R8G8_UNORM -> new int[] { 0, 1 }; + case A16_FLOAT -> new int[] { 0 }; + case R16G16_FLOAT -> new int[] { 0, 1 }; + case A16_UNORM -> new int[] { 0 }; + case R16G16_UNORM -> new int[] { 0, 1 }; + case R16G16B16A16_UNORM -> new int[] { 0, 1, 2, 3 }; + default -> throw new IllegalArgumentException("Unknown Skija ColorType: " + colorType);//$NON-NLS-1$ + }; + } + + public static PaletteData getPaletteData(ColorType colorType) { + return switch (colorType) { + case ALPHA_8 -> new PaletteData(new RGB[] { new RGB(255, 255, 255), new RGB(0, 0, 0) }); + case RGB_565 -> new PaletteData(0xF800, 0x07E0, 0x001F); + case ARGB_4444 -> new PaletteData(0x0F00, 0x00F0, 0x000F); + case RGBA_8888 -> new PaletteData(0xFF000000, 0x00FF0000, 0x0000FF00); + case BGRA_8888 -> new PaletteData(0x0000FF00, 0x00FF0000, 0xFF000000); + case RGBA_F16 -> new PaletteData(new RGB[] { new RGB(255, 0, 0), new RGB(0, 255, 0), new RGB(0, 0, 255) }); + case RGBA_F32 -> + new PaletteData(new RGB[] { new RGB(255, 165, 0), new RGB(0, 255, 255), new RGB(128, 0, 128) }); + default -> throw new IllegalArgumentException("Unknown Skija ColorType: " + colorType);//$NON-NLS-1$ + }; + } + + public static int getImageDepth(ColorType colorType) { + return switch (colorType) { + case ALPHA_8 -> 8; + case RGB_565 -> 16; + case ARGB_4444 -> 16; + case RGBA_8888 -> 32; + case BGRA_8888 -> 32; + case RGBA_F16 -> 64; + case RGBA_F32 -> 128; + default -> 32; + }; + } +} diff --git a/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SwtToSkiaImageConverter.java b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SwtToSkiaImageConverter.java new file mode 100644 index 00000000000..d3aac665202 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/graphics/SwtToSkiaImageConverter.java @@ -0,0 +1,103 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * Contributors: + * SAP SE and others - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.internal.graphics; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.PaletteData; +import org.eclipse.swt.internal.skia.SkiaResources; + +import io.github.humbleui.skija.ColorAlphaType; +import io.github.humbleui.skija.ColorType; +import io.github.humbleui.skija.ImageInfo; + +public class SwtToSkiaImageConverter { + public static io.github.humbleui.skija.Image convertSWTImageToSkijaImage(Image swtImage, int zoom, + SkiaResources resources) { + io.github.humbleui.skija.Image img = null; + final var cached = resources.getCachedImage(swtImage, zoom); + if (cached != null && !cached.isClosed()) { + return cached; + } + final ImageData imageData = swtImage.getImageData(zoom); + img = convertSWTImageToSkijaImage(imageData); + resources.cacheImage(swtImage, zoom, img); + return img; + } + + public static io.github.humbleui.skija.Image convertSWTImageToSkijaImage(ImageData imageData) { + final int width = imageData.width; + final int height = imageData.height; + ColorType colType = getColorType(imageData); + if (colType.equals(ColorType.UNKNOWN) || imageData.alphaData != null || colType.equals(ColorType.ALPHA_8)) { + final byte[] bytes = RGBAEncoder.encode(imageData); + colType = ColorType.RGBA_8888; + final ImageInfo imageInfo = new ImageInfo(width, height, colType, ColorAlphaType.UNPREMUL); + return io.github.humbleui.skija.Image.makeRasterFromBytes(imageInfo, bytes, imageData.width * 4); + } + final ImageInfo imageInfo = new ImageInfo(width, height, colType, ColorAlphaType.UNPREMUL); + return io.github.humbleui.skija.Image.makeRasterFromBytes(imageInfo, imageData.data, imageData.width * 4); + } + + public static ColorType getColorType(ImageData imageData) { + final PaletteData palette = imageData.palette; + if (imageData.getTransparencyType() == SWT.TRANSPARENCY_MASK) { + return ColorType.UNKNOWN; + } + if (palette.isDirect) { + final int redMask = palette.redMask; + final int greenMask = palette.greenMask; + final int blueMask = palette.blueMask; + if (redMask == 0xFF0000 && greenMask == 0x00FF00 && blueMask == 0x0000FF) { + return ColorType.UNKNOWN; + } + if (redMask == 0xFF000000 && greenMask == 0x00FF0000 && blueMask == 0x0000FF00) { + return ColorType.RGBA_8888; + } + if (redMask == 0xF800 && greenMask == 0x07E0 && blueMask == 0x001F) { + return ColorType.RGB_565; + } + if (redMask == 0xF000 && greenMask == 0x0F00 && blueMask == 0x00F0) { + return ColorType.ARGB_4444; + } + if (redMask == 0x0000FF00 && greenMask == 0x00FF0000 && blueMask == 0xFF000000) { + return ColorType.BGRA_8888; + } + if (redMask == 0x3FF00000 && greenMask == 0x000FFC00 && blueMask == 0x000003FF) { + return ColorType.RGBA_1010102; + } + if (redMask == 0x000003FF && greenMask == 0x000FFC00 && blueMask == 0x3FF00000) { + return ColorType.BGRA_1010102; + } + if (redMask == 0xFF && greenMask == 0xFF00 && blueMask == 0xFF0000) { + return ColorType.UNKNOWN; + } + } else { + if (imageData.depth == 8 && palette.colors != null && palette.colors.length <= 256) { + return ColorType.ALPHA_8; + } + switch (imageData.depth) { + case 16: + return ColorType.ARGB_4444; + case 24: + return ColorType.RGB_888X; + case 32: + return ColorType.RGBA_8888; + default: + break; + } + } + return ColorType.UNKNOWN; + } +} \ No newline at end of file diff --git a/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/skia/DpiScalerUtil.java b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/skia/DpiScalerUtil.java new file mode 100644 index 00000000000..2b127edfc54 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/skia/DpiScalerUtil.java @@ -0,0 +1,130 @@ +package org.eclipse.swt.internal.skia; + +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.internal.canvasext.IDpiScaler; +import org.eclipse.swt.widgets.Display; + +public class DpiScalerUtil implements IDpiScaler { + + private final IDpiScaler scaler; + + public DpiScalerUtil(IDpiScaler scaler) { + this.scaler = scaler; + } + + public DpiScalerUtil(int nativeZoom) { + this.scaler = () -> nativeZoom; + } + + public DpiScalerUtil(SkiaResources resources) { + this.scaler = resources.getScaler(); + } + + public IDpiScaler getScaler() { + return scaler; + } + + /** + * Scales a value up according to the native zoom factor. Uses integer + * arithmetic with rounding for precise results (e.g. for nativeZoom=125). + */ + private int scaleUp(int value) { + return (value * getNativeZoom() + 50) / 100; + } + + /** + * Scales a value down according to the native zoom factor. Uses integer + * arithmetic with rounding for precise results (e.g. for nativeZoom=125). + */ + private int scaleDown(int value) { + return (value * 100 + getNativeZoom() / 2) / getNativeZoom(); + } + + public int getZoomedFontSize(int fontSize) { + // dpi to inch + fontSize = (fontSize * Display.getDefault().getDPI().y) / 72; + return scaleUp(fontSize); + } + + public Point scaleSize(int x, int y) { + return new Point(scaleUp(x), scaleUp(y)); + } + + public Point scaleSurfaceSize(int width, int height) { + return new Point(scaleUp(width), scaleUp(height)); + } + + public float autoScaleUp(float f) { + return getNativeZoom() * f / 100f; + } + + public int autoScaleUp(int value) { + return scaleUp(value); + } + + public float autoScaleDown(float width) { + return (100f * width) / getNativeZoom(); + } + + /** + * Scales a float array down according to the native zoom factor. Each value is + * divided by nativeZoom/100, preserving float precision. + * + * @param values the array to scale down + * @return a new array with all values scaled down, or null if input is null + */ + public float[] autoScaleDown(float[] values) { + if (values == null) { + return null; + } + final float[] result = new float[values.length]; + final float factor = 100f / getNativeZoom(); + for (int i = 0; i < values.length; i++) { + result[i] = values[i] * factor; + } + return result; + } + + /** + * Scales a float value down according to the native zoom factor and rounds to + * the nearest integer. + * + * @param f the value to scale down + * @return the scaled and rounded integer value + */ + public int autoScaleDownToInt(float f) { + return Math.round((100f * f) / getNativeZoom()); + } + + public Point scaleDown(Point point) { + return new Point(scaleDown(point.x), scaleDown(point.y)); + } + + public float[] autoScaleUp(float[] values) { + if (values == null) { + return null; + } + final float[] result = new float[values.length]; + for (int i = 0; i < values.length; i++) { + result[i] = autoScaleUp(values[i]); + } + return result; + } + + public int[] autoScaleUp(int[] values) { + if (values == null) { + return null; + } + final int[] result = new int[values.length]; + for (int i = 0; i < values.length; i++) { + result[i] = autoScaleUp(values[i]); + } + return result; + } + + @Override + public int getNativeZoom() { + return this.scaler.getNativeZoom(); + } + +} diff --git a/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/skia/ISkiaCanvasExtension.java b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/skia/ISkiaCanvasExtension.java new file mode 100644 index 00000000000..e48bc62a428 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/skia/ISkiaCanvasExtension.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.swt.internal.skia; + +import io.github.humbleui.skija.Surface; + +public interface ISkiaCanvasExtension { + + Surface getSurface(); + + SkiaResources getResources(); + + Surface createSupportSurface(int width, int height); + + DpiScalerUtil getScaler(); + +} diff --git a/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/skia/SkiaGlCanvasExtension.java b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/skia/SkiaGlCanvasExtension.java new file mode 100644 index 00000000000..7629fb1d459 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/skia/SkiaGlCanvasExtension.java @@ -0,0 +1,337 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.swt.internal.skia; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.GCExtension; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.internal.canvasext.DpiScaler; +import org.eclipse.swt.internal.canvasext.IExternalCanvasHandler; +import org.eclipse.swt.internal.graphics.RectangleConverter; +import org.eclipse.swt.internal.graphics.SkiaColorConverter; +import org.eclipse.swt.internal.graphics.SkiaGC; +import org.eclipse.swt.opengl.GLData; +import org.eclipse.swt.opengl.GLPaintEventInvoker; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Event; + +import io.github.humbleui.skija.BackendRenderTarget; +import io.github.humbleui.skija.ColorAlphaType; +import io.github.humbleui.skija.ColorInfo; +import io.github.humbleui.skija.ColorSpace; +import io.github.humbleui.skija.ColorType; +import io.github.humbleui.skija.DirectContext; +import io.github.humbleui.skija.FramebufferFormat; +import io.github.humbleui.skija.Image; +import io.github.humbleui.skija.ImageInfo; +import io.github.humbleui.skija.PixelGeometry; +import io.github.humbleui.skija.Surface; +import io.github.humbleui.skija.SurfaceOrigin; +import io.github.humbleui.skija.SurfaceProps; +import io.github.humbleui.types.Rect; + +public final class SkiaGlCanvasExtension extends GLPaintEventInvoker +implements ISkiaCanvasExtension, IExternalCanvasHandler { + + private final DirectContext skijaContext; + private BackendRenderTarget renderTarget; + private Surface surface; + private final Canvas canvas; + private final SkiaResources resources; + private final List redrawCommands = new ArrayList<>(); + + private final DpiScalerUtil scaler; + private Image lastImage; + + List oldRenderTargets = new ArrayList<>(); + + private static final int SAMPLES = 0; + + private record RedrawCommand(Rectangle area) { + } + + public SkiaGlCanvasExtension(Canvas canvas) { + super(canvas, createGLData()); + this.resources = new SkiaResources(canvas, this); + this.scaler = new DpiScalerUtil( new DpiScaler(canvas)); + setCurrent(); + skijaContext = DirectContext.makeGL(); + this.canvas = canvas; + this.canvas.addListener(SWT.Resize, this::onResize); + this.canvas.addListener(SWT.Dispose, e -> onDispose()); + } + + private static GLData createGLData() { + final GLData data = new GLData(); + data.doubleBuffer = true; + data.samples = SAMPLES; + return data; + } + + private void onDispose() { + + if (lastImage != null && !lastImage.isClosed()) { + lastImage.close(); + } + + if (surface != null && !surface.isClosed()) { + surface.close(); + } + if (renderTarget != null && !renderTarget.isClosed()) { + renderTarget.close(); + } + + this.oldRenderTargets.clear(); + + // do not close the skijaContext, this freezes the app. + + } + + private void onResize(Event e) { + + if (this.lastImage != null && !this.lastImage.isClosed()) { + this.lastImage.close(); + this.lastImage = null; + } + + final Rectangle rect = this.canvas.getClientArea(); + + if (surface != null && !surface.isClosed()) { + surface.close(); + surface = null; + } + + final DpiScalerUtil util = new DpiScalerUtil(resources.getScaler()); + + final var scaled = util.scaleSurfaceSize(rect.width, rect.height); + if (renderTarget != null && !renderTarget.isClosed()) { + renderTarget.close(); + } + oldRenderTargets.add(renderTarget); + + renderTarget = BackendRenderTarget.makeGL(scaled.x, scaled.y, /* samples */SAMPLES, /* stencil */0, /* fbid */0, + FramebufferFormat.GR_GL_RGBA8); + surface = Surface.wrapBackendRenderTarget(skijaContext, renderTarget, SurfaceOrigin.BOTTOM_LEFT, + ColorType.RGBA_8888, ColorSpace.getSRGB(), + new SurfaceProps(PixelGeometry.RGB_H).withDeviceIndependentFonts(false)); + if (surface != null) { + surface.getCanvas().clear(getBackgroundForSkia()); + } + + + } + + @Override + public Surface getSurface() { + return surface; + } + + @Override + public void redrawTriggered(int x, int y, int width, int height, boolean all) { + + this.redrawCommands.add(new RedrawCommand(new Rectangle(x, y, width, height))); + super.redrawTriggered(); + + } + + @Override + public void redrawTriggered() { + this.redrawCommands.add(new RedrawCommand(null)); + super.redrawTriggered(); + } + + @Override + public void doPaint(Consumer paintEventSender) { + + if (surface == null) { + return; + } + + final int saveCount = surface.getCanvas().getSaveCount(); + + try { + + setCurrent(); + + Rectangle bounds = null; + + final Rectangle ca = canvas.getClientArea(); + + // canvas not visible, do nothing... + if (ca.isEmpty()) { + skijaContext.flush(); + return; + } + + // for which area do we need to execute the paint events? + // If there are redraw commands, we can limit the area to the union of the + // specified redraw areas. + // If there are no redraw commands, we need to execute the paint events for the + // whole client area. + bounds = extractRedrawArea(ca); + + // if the bounds are not equal to the client area, then the bounds area is + // smaller, which means there is an unchanged area + // which has to stay the same. + // if if we don't even have an image for drawing to the area, we need to execute + // the paint events for the whole client area, + // otherwise we would have an unchanged area which is not drawn at all. + if (!bounds.equals(ca)) { + final boolean imageDrawn = drawImageToSurface(); + if (!imageDrawn) { + bounds = ca; + } + } + + // if the bounds are empty, which means there is no area to draw, then we don't + // draw at all. + if (bounds.isEmpty()) { + skijaContext.flush(); + return; + } + + final DpiScalerUtil util = new DpiScalerUtil(resources.getScaler()); + + // scale the bounds and clip the surface. + final var scaledBounds = RectangleConverter.scaleUpRectangle(util, bounds); + // new save count for the clip, so we can restore to this point in order to stay + // consistent for the future. + this.surface.getCanvas().save(); + this.surface.getCanvas().clipRect(new Rect(scaledBounds.x, scaledBounds.y, + scaledBounds.x + scaledBounds.width, scaledBounds.y + scaledBounds.height)); + this.surface.getCanvas().clear(getBackgroundForSkia()); + + executePaintEvents(paintEventSender, bounds); + // saving the drawn image for the next redraw event. + createLastImageSnapshot(); + // carets will be drawn after the image snapshot. Carets do not belong to the + // redraw logic. + // A blinking caret could be implemented by redrawing the last image and + // then draw the caret on top of it, + // without executing the paint events again. + // Caret support needs more modifications in canvas and caret. Not yet + // supported. + + skijaContext.flush(); + + } finally { + if (surface != null && !surface.getCanvas().isClosed()) { + surface.getCanvas().restoreToCount(saveCount); + } + } + + } + + private void executePaintEvents(Consumer consumer, Rectangle bounds) { + final Event event = new Event(); + event.count = 1; + event.setBounds(bounds); + final SkiaGC gc = new SkiaGC(canvas, this, SWT.None); + event.gc = new GCExtension(gc); + event.display = this.canvas.getDisplay(); + try { + consumer.accept(event); + } finally { + gc.dispose(); + event.gc = null; + } + } + + private void createLastImageSnapshot() { + if (this.lastImage != null && !this.lastImage.isClosed()) { + this.lastImage.close(); + this.lastImage = null; + } + if (surface != null && surface.getCanvas() != null && !surface.getCanvas().isClosed()) { + this.lastImage = surface.makeImageSnapshot(); + } else { + this.lastImage = null; + } + + } + + private boolean drawImageToSurface() { + + if (this.lastImage != null && !this.lastImage.isClosed()) { + this.surface.getCanvas().drawImage(this.lastImage, 0, 0); + return true; + } + + return false; + } + + private Rectangle extractRedrawArea(Rectangle ca) { + + try { + if (this.redrawCommands.isEmpty()) { + return ca; + } + + var area = this.redrawCommands.get(0).area; + + if (area == null) { + this.redrawCommands.clear(); + return ca; + } + + if (this.redrawCommands.size() > 1) { + + for (int i = 1; i < this.redrawCommands.size(); i++) { + final var a = this.redrawCommands.get(i).area; + if (a == null) { + return ca; + } + area = area.union(a); + } + + } + return area.intersection(ca); + + } finally { + this.redrawCommands.clear(); + } + + } + + private int getBackgroundForSkia() { + return SkiaColorConverter.convertSWTColorToSkijaColor(canvas.getBackground()); + } + + @Override + public SkiaResources getResources() { + return this.resources; + } + + @Override + public Surface createSupportSurface(int width, int height) { + final ImageInfo i = new ImageInfo(new ColorInfo(ColorType.RGBA_8888, ColorAlphaType.PREMUL, null), width, + height); + // These support surfaces can cause drastic crashes without any information what happened. + // + return Surface.makeRenderTarget(skijaContext, false, i); + } + + @Override + public DpiScalerUtil getScaler() { + return scaler; + } + + @Override + public void scroll(int destX, int destY, int x, int y, int width, int height, boolean all) { + // TODO lastImage could be used for scrolling and then for the redraw would contain only the new area. + canvas.redraw(); + } + +} diff --git a/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/skia/SkiaResources.java b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/skia/SkiaResources.java new file mode 100644 index 00000000000..94e87b63acc --- /dev/null +++ b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/skia/SkiaResources.java @@ -0,0 +1,506 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.swt.internal.skia; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageVersion; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.internal.canvasext.FontProperties; +import org.eclipse.swt.internal.canvasext.IDpiScaler; +import org.eclipse.swt.internal.skia.cache.ImageKey; +import org.eclipse.swt.internal.skia.cache.ImageTextKey; +import org.eclipse.swt.internal.skia.cache.SplitsTextCache; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; + +import io.github.humbleui.skija.FontEdging; +import io.github.humbleui.skija.FontHinting; +import io.github.humbleui.skija.FontMgr; +import io.github.humbleui.skija.FontSlant; +import io.github.humbleui.skija.FontStyle; +import io.github.humbleui.skija.Typeface; + +public class SkiaResources { + private static final String IMAGE_CACHE = "org.eclipse.swt.internal.skia.ImageCache"; + public static boolean USE_IMAGE_CACHE = false; + public static boolean USE_TEXT_IMAGE_CACHE = true; + + /** Maximum number of entries kept in the text-image LRU cache. */ + private static final int TEXT_IMAGE_CACHE_MAX = 512; + + /** Maximum number of entries kept in the SWT→Skija image LRU cache. */ + private static final int IMAGE_CACHE_MAX = 256; + + /** + * LRU cache that automatically closes the evicted Skija image when the cache + * exceeds its capacity. Must only be accessed from the SWT UI thread. + */ + private static final class LruImageCache extends LinkedHashMap { + private static final long serialVersionUID = -4958211475619287841L; + private final int maxSize; + + LruImageCache(int maxSize) { + super(maxSize + 1, 0.75f, /* accessOrder= */ true); + this.maxSize = maxSize; + } + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + if (size() > maxSize) { + // Close the native Skija image before evicting it from the cache. + final io.github.humbleui.skija.Image image = eldest.getValue(); + if (image != null && !image.isClosed()) { + image.close(); + } + return true; + } + return false; + } + } + + private Color background; + private Color foreground; + private final Canvas canvas; + private Font swtFont; + private io.github.humbleui.skija.Font skiaFont; + + // cache access only from UI thread, so no need for concurrent map + private final Map fontNameMapping = new HashMap<>(); + private final Set unknownFonts = new HashSet<>(); + private final Map fontCache = new HashMap<>(); + private final LruImageCache imageCache = new LruImageCache<>(IMAGE_CACHE_MAX); + private final LruImageCache textImageCache = new LruImageCache<>(TEXT_IMAGE_CACHE_MAX); + private final Map cachedTextSplits = new HashMap<>(); + + // --------------------------------------------------------------------------------- + + private final ISkiaCanvasExtension skiaExtension; + + public SkiaResources(Canvas canvas, ISkiaCanvasExtension skiaExtension) { + + USE_IMAGE_CACHE = System.getProperty(IMAGE_CACHE) == null ? false + : Boolean.parseBoolean(System.getProperty(IMAGE_CACHE)); + + this.canvas = canvas; + this.skiaExtension = skiaExtension; + this.canvas.addListener(SWT.Dispose, e -> resetResources()); + this.canvas.addListener(SWT.ZoomChanged, e -> resetResources()); + this.canvas.addListener(SWT.Resize, e -> resetResources()); + } + + public void setBackground(Color color) { + + if (color == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + this.background = color; + } + + public void setForeground(Color color) { + if (color == null) { + SWT.error(SWT.ERROR_NULL_ARGUMENT); + } + this.foreground = color; + } + + public Color getForeground() { + if (foreground != null && !foreground.isDisposed()) { + return foreground; + } + return canvas.getForeground(); + + } + + public Color getBackground() { + + if (background != null && !background.isDisposed()) { + return background; + } + + return canvas.getBackground(); + } + + public void setFont(Font font) { + if (font != null) { + if (font.isDisposed()) { + SWT.error(SWT.ERROR_INVALID_ARGUMENT); + } + } else { + font = getDefaultFont(); + } + this.swtFont = font; + this.skiaFont = getSkijaFont(font); + + } + + private io.github.humbleui.skija.Font getSkijaFont(org.eclipse.swt.graphics.Font font) { + final FontProperties props = FontProperties.getFontProperties(font); + var cachedFont = fontCache.get(props); + if (cachedFont != null && cachedFont.isClosed()) { + fontCache.remove(props); + cachedFont = null; + } + + io.github.humbleui.skija.Font f = null; + + if (cachedFont == null) { + f = createSkijaFont(props); + fontCache.put(props, f); + return f; + } + + return fontCache.get(props); + } + + private io.github.humbleui.skija.Font createSkijaFont(FontProperties props) { + + FontSlant slant = FontSlant.UPRIGHT; + if (props.lfItalic != 0) { + slant = FontSlant.ITALIC; + } + + final FontStyle style = new FontStyle(props.lfWeight, 5, slant); + io.github.humbleui.skija.Font skijaFont = new io.github.humbleui.skija.Font(extractTypeface(props, style)); + + final var rect = skijaFont.measureText("T"); + final var textHeight = rect.getHeight(); + if (textHeight == 0.0) { + + props.name = Display.getDefault().getSystemFont().getFontData()[0].getName(); + skijaFont = new io.github.humbleui.skija.Font(extractTypeface(props, style)); + } + + if (props.lfWidth != 0) { + final float stretch = (float) ((props.lfWidth / 10.0) + 0.5); + skijaFont.setScaleX(stretch); + } + + int fontSize = (props.lfHeight); + final DpiScalerUtil util = new DpiScalerUtil(getScaler()); + fontSize = util.getZoomedFontSize(fontSize); + + skijaFont.setSize(fontSize); + skijaFont.setEdging(FontEdging.SUBPIXEL_ANTI_ALIAS); + skijaFont.setSubpixel(true); + skijaFont.setHinting(FontHinting.NORMAL); + skijaFont.setAutoHintingForced(true); + + return skijaFont; + } + + private Typeface extractTypeface(FontProperties props, FontStyle style) { + + final FontMgr fm = FontMgr.getDefault(); + var name = props.name; + + if (name == null || name.isEmpty()) { + name = Display.getDefault().getSystemFont().getFontData()[0].getName(); + } + + name = name.trim(); + + if (fontNameMapping.containsKey(name)) { + return fm.matchFamilyStyle(fontNameMapping.get(name), style); + } + + if (unknownFonts.contains(name)) { + return fm.matchFamilyStyle(null, style); + } + + var bestMatch = findBestFit(name); + + if (bestMatch == null) { + bestMatch = findBestFit(name.replaceAll("[^\\p{L}\\p{N}]+", " ")); //$NON-NLS-1$ //$NON-NLS-2$ + } + + if (bestMatch != null) { + fontNameMapping.put(name, bestMatch); + return fm.matchFamilyStyle(bestMatch, style); + } + + if (SWT.getPlatform().equals("win32")) { //$NON-NLS-1$ + // arabic fonts are no longer supported on windows. Also windows falls back to + // arial + if (name.toLowerCase().startsWith("arabic ") || name.toLowerCase().equals("arabic")) { //$NON-NLS-1$ + fontNameMapping.put(name, "Arial"); //$NON-NLS-1$ + return fm.matchFamilyStyle("Arial", style); //$NON-NLS-1$ + } + } + // No matching font found, use system default + unknownFonts.add(name); + return fm.matchFamilyStyle(null, style); + + } + + private static String findBestFit(String name) { + final String[] parts = name.split(" "); //$NON-NLS-1$ + + for (int i = 0; i < parts.length; i++) { + parts[i] = parts[i].trim(); + parts[i] = parts[i].toLowerCase(); + } + + final FontMgr fm = FontMgr.getDefault(); + final var count = fm.getFamiliesCount(); + + String bestMatch = null; + int bestMatchScore = 0; + + for (int i = 0; i < count; i++) { + + final var f = fm.getFamilyName(i); + final var fLower = f.toLowerCase(); + + int score = 0; + for (final String part : parts) { + if (fLower.contains(part)) { + score++; + } + } + + if (score > bestMatchScore) { + bestMatchScore = score; + bestMatch = f; + } + } + return bestMatch; + } + + private org.eclipse.swt.graphics.Font getDefaultFont() { + org.eclipse.swt.graphics.Font originalFont = canvas.getFont(); + + if (originalFont == null || originalFont.isDisposed()) { + originalFont = canvas.getDisplay().getSystemFont(); + } + return originalFont; + } + + public io.github.humbleui.skija.Font getSkiaFont() { + if (skiaFont == null) { + setFont(null); + } + return skiaFont; + } + + public IDpiScaler getScaler() { + return skiaExtension.getScaler(); + } + + private void resetResources() { + + if (this.skiaFont != null && !this.skiaFont.isClosed()) { + this.skiaFont.close(); + } + skiaFont = null; + + fontCache.values().forEach(f -> { + if (!f.isClosed()) { + f.close(); + } + }); + fontCache.clear(); + + imageCache.values().forEach(i -> { + if (!i.isClosed()) { + i.close(); + } + }); + + imageCache.clear(); + + textImageCache.values().forEach(i -> { + if (!i.isClosed()) { + i.close(); + } + }); + + textImageCache.clear(); + cachedTextSplits.clear(); + + fontNameMapping.clear(); + unknownFonts.clear(); + + } + + public Font getFont() { + + if (swtFont != null && !swtFont.isDisposed()) { + return swtFont; + } + swtFont = getDefaultFont(); + + return swtFont; + } + + public void resetBaseColors() { + foreground = null; + background = null; + + } + + public void cacheImage(Image swtImage, int zoom, io.github.humbleui.skija.Image skijaImage) { + if (USE_IMAGE_CACHE) { + final var key = new ImageKey(swtImage, ImageVersion.getVersion(swtImage), zoom); + final var old = imageCache.get(key); + if (old != null && !old.isClosed()) { + old.close(); + } + this.imageCache.put(key, skijaImage); + } + } + + public io.github.humbleui.skija.Image getCachedImage(Image swtImage, int zoom) { + return this.imageCache.get(new ImageKey(swtImage, ImageVersion.getVersion(swtImage), zoom)); + } + + public void cacheTextImage(String text, FontProperties fontProperties, boolean transparent, int background, + int foreground, boolean antiAlias, io.github.humbleui.skija.Image skijaImage) { + if (USE_TEXT_IMAGE_CACHE) { + final var key = new ImageTextKey(text, fontProperties, transparent, background, foreground, antiAlias); + final var old = textImageCache.get(key); + if (old != null && !old.isClosed()) { + old.close(); + } + this.textImageCache.put(key, skijaImage); + } + } + + public io.github.humbleui.skija.Image getTextImage(String text, FontProperties fontProperties, boolean transparent, + int background, int foreground, boolean antialias) { + return this.textImageCache + .get(new ImageTextKey(text, fontProperties, transparent, background, foreground, antialias)); + } + + private static String[] splitString(String text) { + return text.split("\r\n|\n|\r"); //$NON-NLS-1$ + } + + public String[] getTextSplits(String inputText, int flags) { + + final boolean replaceAmpersand = (flags & SWT.DRAW_MNEMONIC) != 0; + final boolean delimiter = (flags & SWT.DRAW_DELIMITER) != 0; + final boolean tabulatorExpansion = (flags & SWT.DRAW_TAB) != 0; + + String[] splits = null; + + if (USE_TEXT_IMAGE_CACHE) { + splits = cachedTextSplits + .get(new SplitsTextCache(inputText, replaceAmpersand, delimiter, tabulatorExpansion)); + } + + String workInputText = inputText; + + if (splits == null) { + if (tabulatorExpansion) { + workInputText = expandTabs(workInputText, 0); + } else { + workInputText = workInputText.replaceAll("\\t", ""); //$NON-NLS-1$ //$NON-NLS-2$ + } + + if (replaceAmpersand) { + workInputText = replaceMnemonics(workInputText); + } + + // replace form feed characters with "\u240C" this is the unicode standard sign + // for form feed. + // unfortunately skia does not even render these form feed characters... + workInputText = workInputText.replace("\f", "\u240C"); //$NON-NLS-1$//$NON-NLS-2$ + + if (delimiter) { + splits = splitString(workInputText); + } else { + splits = new String[] { removeDelimiter(workInputText) }; + } + } + + if (USE_TEXT_IMAGE_CACHE) { + cachedTextSplits.put(new SplitsTextCache(inputText, replaceAmpersand, delimiter, tabulatorExpansion), + splits); + } + return splits; + } + + /** + * Expands tab characters (\t) in the text to position-dependent spaces, so that + * the next character aligns to the next tab stop (every 8 average character + * widths by default). The expansion is based on the current x position and the + * average character width of the font. + * + * @param text The input text containing tab characters + * @param startX The starting x position in pixels (used to calculate tab + * alignment) + * @return The text with tabs expanded to spaces, aligned to the next tab stop + */ + private String expandTabs(String text, int startX) { + final StringBuilder result = new StringBuilder(); + int currentX = 0; + // Measure space width exactly once — it is constant for the current font. + final int spaceWidth = textExtent(" ").x; //$NON-NLS-1$ + final float avgCharWidthF = getSkiaFont().getMetrics()._avgCharWidth; + int avgCharWidth = (int) avgCharWidthF; + if (avgCharWidth <= 0) { + avgCharWidth = spaceWidth > 0 ? spaceWidth : 1; + } + final int tabSpacingPx = 8 * avgCharWidth; + for (int i = 0; i < text.length(); i++) { + final char ch = text.charAt(i); + if (ch == '\t') { + final int offsetInTab = tabSpacingPx > 0 ? (currentX - startX) % tabSpacingPx : 0; + final int nextTabX = currentX + (tabSpacingPx - offsetInTab); + while (currentX < nextTabX) { + result.append(' '); + // reuse the already-measured spaceWidth instead of calling textExtent again + currentX += spaceWidth; + } + } else { + // measureTextWidth avoids creating an intermediate String object per character + final int charWidth = (int) getSkiaFont().measureTextWidth(String.valueOf(ch)); + result.append(ch); + currentX += charWidth; + } + } + return result.toString(); + } + + public Point textExtent(String text, int flags) { + + final float height = getSkiaFont().getMetrics().getHeight(); + final float width = getSkiaFont().measureTextWidth(replaceMnemonics(text)); + return new Point((int) width, (int) height); + } + + public Point textExtent(String string) { + return textExtent(string, SWT.NONE); + } + + private static String removeDelimiter(String inputText) { + return inputText.replaceAll("\r\n|\r|\n", ""); //$NON-NLS-1$//$NON-NLS-2$ + } + + public static String replaceMnemonics(String text) { + final int mnemonicIndex = text.lastIndexOf('&'); + if (mnemonicIndex != -1) { + text = text.replaceAll("&", ""); //$NON-NLS-1$ //$NON-NLS-2$ + // TODO Underline the mnemonic key + // it seems this also does not work in windows with a simple snippet. + } + return text; + } +} \ No newline at end of file diff --git a/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/skia/cache/ImageKey.java b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/skia/cache/ImageKey.java new file mode 100644 index 00000000000..3c5d0bd5289 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/skia/cache/ImageKey.java @@ -0,0 +1,53 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.swt.internal.skia.cache; + +import org.eclipse.swt.graphics.ImageVersion; + +public class ImageKey { + + private final int version; + private final org.eclipse.swt.graphics.Image image; + private final int zoom; + + public ImageKey(org.eclipse.swt.graphics.Image image , ImageVersion version, int zoom) { + this.version = version.getVersion(); + this.image = image; + this.zoom = zoom; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final ImageKey that = (ImageKey) obj; + return version == that.version && + image == that.image && + zoom == that.zoom ; + } + + @Override + public int hashCode() { + int result = Integer.hashCode(version); + result = 31 * result + image.hashCode(); + result = 31 * result + Integer.hashCode(zoom); + return result; + } + + @Override + public String toString() { + return String.format("ImageCache{version=%d, image=%s, zoom=%d}", version, image, zoom); //$NON-NLS-1$ + } +} \ No newline at end of file diff --git a/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/skia/cache/ImageTextKey.java b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/skia/cache/ImageTextKey.java new file mode 100644 index 00000000000..c61107fd322 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/skia/cache/ImageTextKey.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * Contributors: + * SAP SE and others - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.internal.skia.cache; + +import java.util.Objects; + +import org.eclipse.swt.internal.canvasext.FontProperties; + +public class ImageTextKey { + public final String text; + public final FontProperties fontProperties; + public final boolean transparent; + public final int background; + public final int foreground; + public final boolean antiAlias; + + public ImageTextKey(String text, FontProperties fontProperties, boolean transparent, int background, int foreground, boolean antiAlias) { + this.text = text; + this.fontProperties = fontProperties; + this.transparent = transparent; + this.background = background; + this.foreground = foreground; + this.antiAlias = antiAlias; + } + + @Override + public int hashCode() { + + if(transparent) { + return Objects.hash(fontProperties, foreground, text, transparent, antiAlias); + } + return Objects.hash(background, fontProperties, foreground, text, antiAlias); + } + + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ImageTextKey other = (ImageTextKey) obj; + + if(transparent ) { + return Objects.equals(fontProperties, other.fontProperties) + && foreground == other.foreground && Objects.equals(text, other.text) && + other.transparent && antiAlias == other.antiAlias; + } + + return background == other.background && Objects.equals(fontProperties, other.fontProperties) + && foreground == other.foreground && Objects.equals(text, other.text) && antiAlias == other.antiAlias; + } + + + +} \ No newline at end of file diff --git a/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/skia/cache/SplitsTextCache.java b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/skia/cache/SplitsTextCache.java new file mode 100644 index 00000000000..c83a75de747 --- /dev/null +++ b/bundles/org.eclipse.swt.skia/src/org/eclipse/swt/internal/skia/cache/SplitsTextCache.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * Contributors: + * SAP SE and others - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.internal.skia.cache; + +import java.util.Objects; + +/** + * The analysis of texts for the text drawing is quite expensive, so cache and prevent exhaustive analysis. + */ +public class SplitsTextCache { + + public final String text; + public final boolean replaceAmpersand; + public final boolean delimiter; + public final boolean tabulatorExpansion; + + private String[] splits; + + public SplitsTextCache(String text, boolean replaceAmpersand, boolean delimiter, boolean tabulatorExpansion) { + super(); + this.text = text; + this.replaceAmpersand = replaceAmpersand; + this.delimiter = delimiter; + this.tabulatorExpansion = tabulatorExpansion; + } + + public String[] getSplits() { + if (splits == null) { + splits = text.split("\n", -1); //$NON-NLS-1$ + } + return splits; + } + + @Override + public int hashCode() { + return Objects.hash(delimiter, replaceAmpersand, tabulatorExpansion, text); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final SplitsTextCache other = (SplitsTextCache) obj; + return delimiter == other.delimiter && replaceAmpersand == other.replaceAmpersand + && tabulatorExpansion == other.tabulatorExpansion && Objects.equals(text, other.text); + } + + + + + +} diff --git a/bundles/org.eclipse.swt/Eclipse SWT Custom Widgets/common/org/eclipse/swt/custom/CLabel.java b/bundles/org.eclipse.swt/Eclipse SWT Custom Widgets/common/org/eclipse/swt/custom/CLabel.java index f689c98bde4..72b19885086 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT Custom Widgets/common/org/eclipse/swt/custom/CLabel.java +++ b/bundles/org.eclipse.swt/Eclipse SWT Custom Widgets/common/org/eclipse/swt/custom/CLabel.java @@ -136,7 +136,7 @@ public CLabel(Composite parent, int style) { */ private static int checkStyle (int style) { if ((style & SWT.BORDER) != 0) style |= SWT.SHADOW_IN; - int mask = SWT.SHADOW_IN | SWT.SHADOW_OUT | SWT.SHADOW_NONE | SWT.LEFT_TO_RIGHT | SWT.RIGHT_TO_LEFT; + int mask = SWT.SHADOW_IN | SWT.SHADOW_OUT | SWT.SHADOW_NONE | SWT.LEFT_TO_RIGHT | SWT.RIGHT_TO_LEFT | SWT.SKIA; style = style & mask; return style |= SWT.NO_FOCUS | SWT.DOUBLE_BUFFERED; } diff --git a/bundles/org.eclipse.swt/Eclipse SWT OpenGL/cocoa/org/eclipse/swt/opengl/GLPaintEventInvoker.java b/bundles/org.eclipse.swt/Eclipse SWT OpenGL/cocoa/org/eclipse/swt/opengl/GLPaintEventInvoker.java new file mode 100644 index 00000000000..4cb05d079fd --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT OpenGL/cocoa/org/eclipse/swt/opengl/GLPaintEventInvoker.java @@ -0,0 +1,28 @@ +package org.eclipse.swt.opengl; + +import java.util.function.*; + +import org.eclipse.swt.widgets.*; + +/** + * @noreference This class is not intended to be referenced by clients. + */ +public abstract class GLPaintEventInvoker { + + public GLPaintEventInvoker(Canvas canvas, GLData data) { + } + + public void redrawTriggered() { + } + + public abstract void doPaint(Consumer paintEventSender); + + public Object paint(Consumer consumer, long wParam, long lParam) { + return null; + } + + public void setCurrent() { + + } + +} diff --git a/bundles/org.eclipse.swt/Eclipse SWT OpenGL/gtk/org/eclipse/swt/opengl/GLCanvasExtension.java b/bundles/org.eclipse.swt/Eclipse SWT OpenGL/gtk/org/eclipse/swt/opengl/GLCanvasExtension.java new file mode 100644 index 00000000000..e5547f5ac8d --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT OpenGL/gtk/org/eclipse/swt/opengl/GLCanvasExtension.java @@ -0,0 +1,296 @@ +/******************************************************************************* + * Copyright (c) 2000, 2025 IBM Corporation and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.opengl; + +import org.eclipse.swt.*; +import org.eclipse.swt.graphics.*; +import org.eclipse.swt.internal.gtk.*; +import org.eclipse.swt.internal.gtk3.*; +import org.eclipse.swt.internal.opengl.glx.*; +import org.eclipse.swt.widgets.*; + +/** + * GLCanvas is a widget capable of displaying OpenGL content. + * + * @see GLData + * @see OpenGL snippets + * @see Sample code and further information + * + * @noreference This class is not intended to be referenced by clients. + */ + +public abstract class GLCanvasExtension { + long context; + long xWindow; + long glWindow; + XVisualInfo vinfo; + protected final Canvas canvas; + static final int MAX_ATTRIBUTES = 32; + +/** + * Create a GLCanvas widget using the attributes described in the GLData + * object provided. + * + * @param data the requested attributes of the GLCanvas + * + * @exception IllegalArgumentException + *
  • ERROR_NULL_ARGUMENT when the data is null
  • + *
  • ERROR_UNSUPPORTED_DEPTH when the requested attributes cannot be provided
  • + *
+ */ +public GLCanvasExtension (Canvas canvas, GLData data) { + this.canvas = canvas; + if (data == null) SWT.error (SWT.ERROR_NULL_ARGUMENT); + int glxAttrib [] = new int [MAX_ATTRIBUTES]; + int pos = 0; + glxAttrib [pos++] = GLX.GLX_RGBA; + if (data.doubleBuffer) glxAttrib [pos++] = GLX.GLX_DOUBLEBUFFER; + if (data.stereo) glxAttrib [pos++] = GLX.GLX_STEREO; + if (data.redSize > 0) { + glxAttrib [pos++] = GLX.GLX_RED_SIZE; + glxAttrib [pos++] = data.redSize; + } + if (data.greenSize > 0) { + glxAttrib [pos++] = GLX.GLX_GREEN_SIZE; + glxAttrib [pos++] = data.greenSize; + } + if (data.blueSize > 0) { + glxAttrib [pos++] = GLX.GLX_BLUE_SIZE; + glxAttrib [pos++] = data.blueSize; + } + if (data.alphaSize > 0) { + glxAttrib [pos++] = GLX.GLX_ALPHA_SIZE; + glxAttrib [pos++] = data.alphaSize; + } + if (data.depthSize > 0) { + glxAttrib [pos++] = GLX.GLX_DEPTH_SIZE; + glxAttrib [pos++] = data.depthSize; + } + if (data.stencilSize > 0) { + glxAttrib [pos++] = GLX.GLX_STENCIL_SIZE; + glxAttrib [pos++] = data.stencilSize; + } + if (data.accumRedSize > 0) { + glxAttrib [pos++] = GLX.GLX_ACCUM_RED_SIZE; + glxAttrib [pos++] = data.accumRedSize; + } + if (data.accumGreenSize > 0) { + glxAttrib [pos++] = GLX.GLX_ACCUM_GREEN_SIZE; + glxAttrib [pos++] = data.accumGreenSize; + } + if (data.accumBlueSize > 0) { + glxAttrib [pos++] = GLX.GLX_ACCUM_BLUE_SIZE; + glxAttrib [pos++] = data.accumBlueSize; + } + if (data.accumAlphaSize > 0) { + glxAttrib [pos++] = GLX.GLX_ACCUM_ALPHA_SIZE; + glxAttrib [pos++] = data.accumAlphaSize; + } + if (data.sampleBuffers > 0) { + glxAttrib [pos++] = GLX.GLX_SAMPLE_BUFFERS; + glxAttrib [pos++] = data.sampleBuffers; + } + if (data.samples > 0) { + glxAttrib [pos++] = GLX.GLX_SAMPLES; + glxAttrib [pos++] = data.samples; + } + glxAttrib [pos++] = 0; + GTK.gtk_widget_realize (canvas.handle); + long window = GTK3.gtk_widget_get_window (canvas.handle); + + long xDisplay = GDK.gdk_x11_display_get_xdisplay(GDK.gdk_window_get_display(window)); + long infoPtr = GLX.glXChooseVisual (xDisplay, OS.XDefaultScreen (xDisplay), glxAttrib); + if (infoPtr == 0) { + canvas.dispose (); + SWT.error (SWT.ERROR_UNSUPPORTED_DEPTH); + } + vinfo = new XVisualInfo (); + GLX.memmove (vinfo, infoPtr, XVisualInfo.sizeof); + OS.XFree (infoPtr); + long screen = GDK.gdk_screen_get_default (); + long gdkvisual = GDK.gdk_x11_screen_lookup_visual (screen, vinfo.visualid); + long share = data.shareContext != null ? data.shareContext.context : 0; + context = GLX.glXCreateContext (xDisplay, vinfo, share, true); + if (context == 0) SWT.error (SWT.ERROR_NO_HANDLES); + GdkWindowAttr attrs = new GdkWindowAttr (); + attrs.width = 1; + attrs.height = 1; + attrs.event_mask = GDK.GDK_KEY_PRESS_MASK | GDK.GDK_KEY_RELEASE_MASK | + GDK.GDK_FOCUS_CHANGE_MASK | GDK.GDK_POINTER_MOTION_MASK | + GDK.GDK_BUTTON_PRESS_MASK | GDK.GDK_BUTTON_RELEASE_MASK | + GDK.GDK_ENTER_NOTIFY_MASK | GDK.GDK_LEAVE_NOTIFY_MASK | + GDK.GDK_EXPOSURE_MASK | GDK.GDK_POINTER_MOTION_HINT_MASK; + attrs.window_type = GDK.GDK_WINDOW_CHILD; + attrs.visual = gdkvisual; + glWindow = GTK3.gdk_window_new (window, attrs, GDK.GDK_WA_VISUAL); + GDK.gdk_window_set_user_data (glWindow, canvas.handle); + if ((canvas.getStyle() & SWT.NO_BACKGROUND) != 0) { + //TODO: implement this on GTK3 as pixmaps are gone. + } + + if (GTK.GTK4) { + // TODO: Enable when the GdkWindow to GdkSurface changes are in + //xWindow = GDK.gdk_x11_surface_get_xid(glWindow); + } else { + xWindow = GDK.gdk_x11_window_get_xid (glWindow); + } + + GDK.gdk_window_show (glWindow); + + Listener listener = event -> { + switch (event.type) { + case SWT.Paint: + /** + * Bug in MESA. MESA does some nasty sort of polling to try + * and ensure that their buffer sizes match the current X state. + * This state can be updated using glViewport(). + * FIXME: There has to be a better way of doing this. + */ + int [] viewport = new int [4]; + GLX.glGetIntegerv (GLX.GL_VIEWPORT, viewport); + GLX.glViewport (viewport [0],viewport [1],viewport [2],viewport [3]); + break; + case SWT.Resize: + Rectangle clientArea = canvas.getClientArea(); + GDK.gdk_window_move (glWindow, clientArea.x, clientArea.y); + GDK.gdk_window_resize (glWindow, clientArea.width, clientArea.height); + break; + case SWT.Dispose: + long window1 = GTK3.gtk_widget_get_window (canvas.handle); + long xDisplay1 = gdk_x11_display_get_xdisplay (window1); + if (context != 0) { + if (GLX.glXGetCurrentContext () == context) { + GLX.glXMakeCurrent (xDisplay1, 0, 0); + } + GLX.glXDestroyContext (xDisplay1, context); + context = 0; + } + if (glWindow != 0) { + GDK.gdk_window_destroy (glWindow); + glWindow = 0; + } + break; + } + }; + canvas.addListener (SWT.Resize, listener); + canvas.addListener (SWT.Paint, listener); + canvas.addListener (SWT.Dispose, listener); +} + +/** + * Returns a GLData object describing the created context. + * + * @return GLData description of the OpenGL context attributes + * @exception SWTException
    + *
  • ERROR_WIDGET_DISPOSED - if the receiver has been disposed
  • + *
  • ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver
  • + *
+ */ +public GLData getGLData () { + checkWidget (); + long window = GTK3.gtk_widget_get_window (canvas.handle); + long xDisplay = gdk_x11_display_get_xdisplay (window); + GLData data = new GLData (); + int [] value = new int [1]; + GLX.glXGetConfig (xDisplay, vinfo, GLX.GLX_DOUBLEBUFFER, value); + data.doubleBuffer = value [0] != 0; + GLX.glXGetConfig (xDisplay, vinfo, GLX.GLX_STEREO, value); + data.stereo = value [0] != 0; + GLX.glXGetConfig (xDisplay, vinfo, GLX.GLX_RED_SIZE, value); + data.redSize = value [0]; + GLX.glXGetConfig (xDisplay, vinfo, GLX.GLX_GREEN_SIZE, value); + data.greenSize = value [0]; + GLX.glXGetConfig (xDisplay, vinfo, GLX.GLX_BLUE_SIZE, value); + data.blueSize = value [0]; + GLX.glXGetConfig (xDisplay, vinfo, GLX.GLX_ALPHA_SIZE, value); + data.alphaSize = value [0]; + GLX.glXGetConfig (xDisplay, vinfo, GLX.GLX_DEPTH_SIZE, value); + data.depthSize = value [0]; + GLX.glXGetConfig (xDisplay, vinfo, GLX.GLX_STENCIL_SIZE, value); + data.stencilSize = value [0]; + GLX.glXGetConfig (xDisplay, vinfo, GLX.GLX_ACCUM_RED_SIZE, value); + data.accumRedSize = value [0]; + GLX.glXGetConfig (xDisplay, vinfo, GLX.GLX_ACCUM_GREEN_SIZE, value); + data.accumGreenSize = value [0]; + GLX.glXGetConfig (xDisplay, vinfo, GLX.GLX_ACCUM_BLUE_SIZE, value); + data.accumBlueSize = value [0]; + GLX.glXGetConfig (xDisplay, vinfo, GLX.GLX_ACCUM_ALPHA_SIZE, value); + data.accumAlphaSize = value [0]; + GLX.glXGetConfig (xDisplay, vinfo, GLX.GLX_SAMPLE_BUFFERS, value); + data.sampleBuffers = value [0]; + GLX.glXGetConfig (xDisplay, vinfo, GLX.GLX_SAMPLES, value); + data.samples = value [0]; + return data; +} + +/** + * Returns a boolean indicating whether the receiver's OpenGL context + * is the current context. + * + * @return true if the receiver holds the current OpenGL context, + * false otherwise + * @exception SWTException
    + *
  • ERROR_WIDGET_DISPOSED - if the receiver has been disposed
  • + *
  • ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver
  • + *
+ */ +public boolean isCurrent () { + checkWidget (); + return GLX.glXGetCurrentContext () == context; +} + +/** + * Sets the OpenGL context associated with this GLCanvas to be the + * current GL context. + * + * @exception SWTException
    + *
  • ERROR_WIDGET_DISPOSED - if the receiver has been disposed
  • + *
  • ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver
  • + *
+ */ +public void setCurrent () { + checkWidget (); + if (GLX.glXGetCurrentContext () == context) return; + long window = GTK3.gtk_widget_get_window (canvas.handle); + long xDisplay = gdk_x11_display_get_xdisplay (window); + GLX.glXMakeCurrent (xDisplay, xWindow, context); +} + +/** + * Swaps the front and back color buffers. + * + * @exception SWTException
    + *
  • ERROR_WIDGET_DISPOSED - if the receiver has been disposed
  • + *
  • ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver
  • + *
+ */ +public void swapBuffers () { + checkWidget (); + long window = GTK3.gtk_widget_get_window (canvas.handle); + long xDisplay = gdk_x11_display_get_xdisplay (window); + GLX.glXSwapBuffers (xDisplay, xWindow); +} + +private long gdk_x11_display_get_xdisplay(long window) { + return GDK.gdk_x11_display_get_xdisplay(GDK.gdk_window_get_display(window)); +} + +private void checkWidget () { + Display display = canvas.getDisplay(); + if (display == null) SWT.error (SWT.ERROR_WIDGET_DISPOSED); + if (display.getThread() != Thread.currentThread ()) SWT.error (SWT.ERROR_THREAD_INVALID_ACCESS); + if (canvas.isDisposed()) SWT.error (SWT.ERROR_WIDGET_DISPOSED); +} + +} diff --git a/bundles/org.eclipse.swt/Eclipse SWT OpenGL/gtk/org/eclipse/swt/opengl/GLPaintEventInvoker.java b/bundles/org.eclipse.swt/Eclipse SWT OpenGL/gtk/org/eclipse/swt/opengl/GLPaintEventInvoker.java new file mode 100644 index 00000000000..5a61ea68929 --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT OpenGL/gtk/org/eclipse/swt/opengl/GLPaintEventInvoker.java @@ -0,0 +1,47 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.swt.opengl; + +import java.util.function.*; + +import org.eclipse.swt.widgets.*; +/** + * @noreference This class is not intended to be referenced by clients. + */ +public abstract class GLPaintEventInvoker extends GLCanvasExtension { + /** + * @noreference This class is not intended to be referenced by clients. + */ + public GLPaintEventInvoker(Canvas canvas, GLData data) { + super(canvas, data); + } + + public Object paint(Consumer paintEventSender,long arg1, long arg2) { + + if (canvas.isDisposed()) + return null; + + if(!isCurrent()) + { + setCurrent(); + } + doPaint(paintEventSender); + swapBuffers(); + + return null; + } + + public abstract void doPaint(Consumer paintEventSender); + + public void redrawTriggered() { + } + +} diff --git a/bundles/org.eclipse.swt/Eclipse SWT OpenGL/win32/org/eclipse/swt/opengl/GLCanvasExtension.java b/bundles/org.eclipse.swt/Eclipse SWT OpenGL/win32/org/eclipse/swt/opengl/GLCanvasExtension.java new file mode 100644 index 00000000000..3a5a0898307 --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT OpenGL/win32/org/eclipse/swt/opengl/GLCanvasExtension.java @@ -0,0 +1,206 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.swt.opengl; + +import org.eclipse.swt.*; +import org.eclipse.swt.internal.opengl.win32.*; +import org.eclipse.swt.internal.win32.*; +import org.eclipse.swt.widgets.*; + +/** + * @noreference This class is not intended to be referenced by clients. + */ +public abstract class GLCanvasExtension { + + long context; + int pixelFormat; + private Canvas canvas; + static final String USE_OWNDC_KEY = "org.eclipse.swt.internal.win32.useOwnDC"; //$NON-NLS-1$ +/** + * Create a GLCanvas widget using the attributes described in the GLData + * object provided. + * + * @param data the requested attributes of the GLCanvas + * +* @noreference This method is not intended to be referenced by clients. + * + * @exception IllegalArgumentException + *
  • ERROR_NULL_ARGUMENT when the data is null
  • + *
  • ERROR_UNSUPPORTED_DEPTH when the requested attributes cannot be provided
  • + *
+ */ +public GLCanvasExtension (Canvas canvas, GLData data) { + this.canvas = canvas; + + canvas.getParent().getDisplay ().setData (USE_OWNDC_KEY, false); + if (data == null) SWT.error (SWT.ERROR_NULL_ARGUMENT); + PIXELFORMATDESCRIPTOR pfd = new PIXELFORMATDESCRIPTOR (); + pfd.nSize = (short) PIXELFORMATDESCRIPTOR.sizeof; + pfd.nVersion = 1; + pfd.dwFlags = WGL.PFD_DRAW_TO_WINDOW | WGL.PFD_SUPPORT_OPENGL; + pfd.dwLayerMask = WGL.PFD_MAIN_PLANE; + pfd.iPixelType = (byte) WGL.PFD_TYPE_RGBA; + if (data.doubleBuffer) pfd.dwFlags |= WGL.PFD_DOUBLEBUFFER; + if (data.stereo) pfd.dwFlags |= WGL.PFD_STEREO; + pfd.cRedBits = (byte) data.redSize; + pfd.cGreenBits = (byte) data.greenSize; + pfd.cBlueBits = (byte) data.blueSize; + pfd.cAlphaBits = (byte) data.alphaSize; + pfd.cDepthBits = (byte) data.depthSize; + pfd.cStencilBits = (byte) data.stencilSize; + pfd.cAccumRedBits = (byte) data.accumRedSize; + pfd.cAccumGreenBits = (byte) data.accumGreenSize; + pfd.cAccumBlueBits = (byte) data.accumBlueSize; + pfd.cAccumAlphaBits = (byte) data.accumAlphaSize; + pfd.cAccumBits = (byte) (pfd.cAccumRedBits + pfd.cAccumGreenBits + pfd.cAccumBlueBits + pfd.cAccumAlphaBits); + + //FIXME - use wglChoosePixelFormatARB +// if (data.sampleBuffers > 0) { +// wglAttrib [pos++] = WGL.WGL_SAMPLE_BUFFERS_ARB; +// wglAttrib [pos++] = data.sampleBuffers; +// } +// if (data.samples > 0) { +// wglAttrib [pos++] = WGL.WGL_SAMPLES_ARB; +// wglAttrib [pos++] = data.samples; +// } + + long hDC = OS.GetDC (canvas.handle); + pixelFormat = WGL.ChoosePixelFormat (hDC, pfd); + if (pixelFormat == 0 || !WGL.SetPixelFormat (hDC, pixelFormat, pfd)) { + OS.ReleaseDC (canvas.handle, hDC); + SWT.error (SWT.ERROR_UNSUPPORTED_DEPTH); + } + context = WGL.wglCreateContext (hDC); + if (context == 0) { + OS.ReleaseDC (canvas.handle, hDC); + SWT.error (SWT.ERROR_NO_HANDLES); + } + OS.ReleaseDC (canvas.handle, hDC); + if (data.shareContext != null) { + WGL.wglShareLists (data.shareContext.context, context); + } + + Listener listener = event -> { + switch (event.type) { + case SWT.Dispose: + WGL.wglDeleteContext (context); + break; + } + }; + canvas.addListener (SWT.Dispose, listener); +} + +public Canvas getCanvas() { + return canvas; +} + +static int checkStyle(Composite parent, int style) { + if (parent != null) { + parent.getDisplay ().setData (USE_OWNDC_KEY, true); + } + return style; +} + +/** + * Returns a GLData object describing the created context. + * +* @noreference This method is not intended to be referenced by clients. +* + * @return GLData description of the OpenGL context attributes + * @exception SWTException
    + *
  • ERROR_WIDGET_DISPOSED - if the receiver has been disposed
  • + *
  • ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver
  • + *
+ */ +public GLData getGLData () { + checkWidget (); + GLData data = new GLData (); + PIXELFORMATDESCRIPTOR pfd = new PIXELFORMATDESCRIPTOR (); + long hDC = OS.GetDC (canvas.handle); + WGL.DescribePixelFormat (hDC, pixelFormat, PIXELFORMATDESCRIPTOR.sizeof, pfd); + OS.ReleaseDC (canvas.handle, hDC); + data.doubleBuffer = (pfd.dwFlags & WGL.PFD_DOUBLEBUFFER) != 0; + data.stereo = (pfd.dwFlags & WGL.PFD_STEREO) != 0; + data.redSize = pfd.cRedBits; + data.greenSize = pfd.cGreenBits; + data.blueSize = pfd.cBlueBits; + data.alphaSize = pfd.cAlphaBits; + data.depthSize = pfd.cDepthBits; + data.stencilSize = pfd.cStencilBits; + data.accumRedSize = pfd.cAccumRedBits; + data.accumGreenSize = pfd.cAccumGreenBits; + data.accumBlueSize = pfd.cAccumBlueBits; + data.accumAlphaSize = pfd.cAccumAlphaBits; + return data; +} + +/** + * Returns a boolean indicating whether the receiver's OpenGL context + * is the current context. + * + * @return true if the receiver holds the current OpenGL context, + * false otherwise + * @exception SWTException
    + *
  • ERROR_WIDGET_DISPOSED - if the receiver has been disposed
  • + *
  • ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver
  • + *
+ * @noreference This method is not intended to be referenced by clients. + */ +public boolean isCurrent () { + checkWidget (); + return WGL.wglGetCurrentContext () == context; +} + +/** + * Sets the OpenGL context associated with this GLCanvas to be the + * current GL context. + * + * @exception SWTException
    + *
  • ERROR_WIDGET_DISPOSED - if the receiver has been disposed
  • + *
  • ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver
  • + *
+ * + * @noreference This method is not intended to be referenced by clients. + * + */ +public void setCurrent () { + checkWidget (); + if (WGL.wglGetCurrentContext () == context) return; + long hDC = OS.GetDC (canvas.handle); + WGL.wglMakeCurrent (hDC, context); + OS.ReleaseDC (canvas.handle, hDC); +} + +/** + * Swaps the front and back color buffers. + * + * @exception SWTException
    + *
  • ERROR_WIDGET_DISPOSED - if the receiver has been disposed
  • + *
  • ERROR_THREAD_INVALID_ACCESS - if not called from the thread that created the receiver
  • + *
+ * @noreference This method is not intended to be referenced by clients. + */ +public void swapBuffers () { + checkWidget (); + long hDC = OS.GetDC (canvas.handle); + WGL.SwapBuffers (hDC); + OS.ReleaseDC (canvas.handle, hDC); +} + +private void checkWidget() { + Display display = canvas.getDisplay(); + if (display == null) SWT.error (SWT.ERROR_WIDGET_DISPOSED); + if (display.getThread() != Thread.currentThread ()) SWT.error (SWT.ERROR_THREAD_INVALID_ACCESS); + if (canvas.isDisposed()) SWT.error (SWT.ERROR_WIDGET_DISPOSED); +} + + +} diff --git a/bundles/org.eclipse.swt/Eclipse SWT OpenGL/win32/org/eclipse/swt/opengl/GLPaintEventInvoker.java b/bundles/org.eclipse.swt/Eclipse SWT OpenGL/win32/org/eclipse/swt/opengl/GLPaintEventInvoker.java new file mode 100644 index 00000000000..ad42451d7af --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT OpenGL/win32/org/eclipse/swt/opengl/GLPaintEventInvoker.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.swt.opengl; + +import java.util.function.*; + +import org.eclipse.swt.widgets.*; + +/** + * @noreference This class is not intended to be referenced by clients. + */ +public abstract class GLPaintEventInvoker extends GLCanvasExtension { + + private boolean redrawTriggered; + + public GLPaintEventInvoker(Canvas canvas, GLData data) { + super(canvas, data); + } + + /** + * @noreference This method is not intended to be referenced by clients. + */ + public Object paint(Consumer consumer, long wParam, long lParam) { + + var canvas = getCanvas(); + + if (canvas.isDisposed()) + return null; + this.redrawTriggered = false; + doPaint(consumer); + swapBuffers(); + + if (redrawTriggered) { + canvas.redraw(); + redrawTriggered = false; + return 0; + } + return null; + + } + + /** + * @noreference This method is not intended to be referenced by clients. + */ + public void redrawTriggered() { + this.redrawTriggered = true; + } + + /** + * @noreference This method is not intended to be referenced by clients. + */ + public abstract void doPaint(Consumer paintEventSender); + +} diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cairo/org/eclipse/swt/graphics/Pattern.java b/bundles/org.eclipse.swt/Eclipse SWT/cairo/org/eclipse/swt/graphics/Pattern.java index cb2850bc151..51a1578595f 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/cairo/org/eclipse/swt/graphics/Pattern.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/cairo/org/eclipse/swt/graphics/Pattern.java @@ -37,6 +37,8 @@ */ public class Pattern extends Resource { + private PatternProperties properties; + /** * the OS resource for the Pattern * (Warning: This field is platform dependent) @@ -90,6 +92,7 @@ public Pattern(Device device, Image image) { Cairo.cairo_pattern_set_extend(handle, Cairo.CAIRO_EXTEND_REPEAT); surface = image.surface; init(); + properties = new PatternProperties(image); } /** @@ -181,6 +184,7 @@ public Pattern(Device device, float x1, float y1, float x2, float y2, Color colo GC.setCairoPatternColor(handle, 1, color2, alpha2); Cairo.cairo_pattern_set_extend(handle, Cairo.CAIRO_EXTEND_REPEAT); init(); + properties = new PatternProperties(x1,y1,x2,y2,color1,alpha1,color2,alpha2); } @Override @@ -216,4 +220,8 @@ public String toString() { return "Pattern {" + handle + "}"; } +PatternProperties getProperties() { + return properties; +} + } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/FontMetrics.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/FontMetrics.java index befde38c747..f15f39c1894 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/FontMetrics.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/FontMetrics.java @@ -24,7 +24,7 @@ * @see GC#getFontMetrics * @see Sample code and further information */ -public final class FontMetrics { +public sealed class FontMetrics permits FontMetricsExtension{ int ascent, descent, leading, height; double averageCharWidth; diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/GC.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/GC.java index 8d619bd4eb5..bafc679d490 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/GC.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/GC.java @@ -59,7 +59,7 @@ * @see SWT Examples: GraphicsExample, PaintExample * @see Sample code and further information */ -public final class GC extends Resource { +public sealed class GC extends Resource permits GCExtension { /** * the handle to the OS device context * (Warning: This field is platform dependent) diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/Image.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/Image.java index 779b593ee2e..6b46c98d30e 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/Image.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/Image.java @@ -1945,5 +1945,13 @@ private Optional loadImageDataAtExactSize(int targetWidth, int target } } +/** + * Not supported on Cocoa. + * @return the version of the image. + */ +ImageVersion getImageVersion() { + return null; +} + } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/Pattern.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/Pattern.java index 6daea06f024..07336537ef8 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/Pattern.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/Pattern.java @@ -229,4 +229,8 @@ public String toString() { return "Pattern {" + (color != null ? color.id : gradient.id) + "}"; } +PatternProperties getProperties() { + return null; +} + } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/Region.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/Region.java index 8eeb98c0a49..320d396142b 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/Region.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/graphics/Region.java @@ -867,4 +867,8 @@ public String toString () { if (isDisposed()) return "Region {*DISPOSED*}"; return "Region {" + handle + "}"; } + +RegionLog getLog() { + return null; +} } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/internal/canvasext/DpiScaler.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/internal/canvasext/DpiScaler.java new file mode 100644 index 00000000000..380cac9bbec --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/internal/canvasext/DpiScaler.java @@ -0,0 +1,21 @@ +package org.eclipse.swt.internal.canvasext; + +import org.eclipse.swt.graphics.*; +import org.eclipse.swt.widgets.*; + +/** + * + * Provides utility methods for the canvas extension to scale values according + * to the current DPI settings of the OS. This is used internally to scale all + * values that are used for drawing and layout calculations. + */ +public class DpiScaler implements IDpiScaler { + + public DpiScaler(Canvas canvas) { + } + + public int getNativeZoom() { + return 100; + } + +} \ No newline at end of file diff --git a/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/internal/canvasext/FontProperties.java b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/internal/canvasext/FontProperties.java new file mode 100644 index 00000000000..2162e126810 --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/cocoa/org/eclipse/swt/internal/canvasext/FontProperties.java @@ -0,0 +1,25 @@ +package org.eclipse.swt.internal.canvasext; + +import org.eclipse.swt.graphics.*; + +public class FontProperties { + + public int lfHeight; + public int lfWidth = -1; + public int lfEscapement; + public int lfOrientation; + public int lfWeight = -1; + public byte lfItalic; + public byte lfUnderline; + public byte lfStrikeOut; + public String name; + + private FontProperties() { + + } + + public static FontProperties getFontProperties(Font font) { + return null; + + } +} diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/SWT.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/SWT.java index bf4ebc4d6a8..9eacedb5909 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/SWT.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/SWT.java @@ -2564,6 +2564,19 @@ public class SWT { */ public static final int WEBKIT = 1 << 16; + /** + * Experimental This API is experimental and may change or be removed in future releases. + * Style constant specifying that a Canvas should use Skia. Only works for windows and linux. + * for rendering its content (value is 1<<23). + *

Used By:

+ *
    + *
  • Canvas
  • + *
+ * + * @since 3.134 + */ + public static final int SKIA = 1 << 23; + /** * Style constant specifying that a Browser should use Edge (WebView2) * for rendering its content (value is 1<<18). @@ -5000,3 +5013,4 @@ public static void error (int code, Throwable throwable, String detail) { } } } + diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/FontMetricsExtension.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/FontMetricsExtension.java new file mode 100644 index 00000000000..89f9306f7ff --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/FontMetricsExtension.java @@ -0,0 +1,61 @@ +package org.eclipse.swt.graphics; + +import org.eclipse.swt.internal.canvasext.*; + +/** + * @noreference This class is not intended to be referenced by clients. + */ +public final class FontMetricsExtension extends FontMetrics { + + private IExternalFontMetrics externalMetrics; + + /** + * @noreference This constructor is not intended to be referenced by clients. + * @param extMetrics + */ + public FontMetricsExtension(IExternalFontMetrics extMetrics) { + this.externalMetrics = extMetrics; + } + + @Override + public int getAscent() { + return externalMetrics.getAscent(); + } + + @Override + public double getAverageCharacterWidth() { + return externalMetrics.getAverageCharacterWidth(); + } + + @Override + @Deprecated + public int getAverageCharWidth() { + return externalMetrics.getAverageCharWidth(); + } + + @Override + public int getDescent() { + return externalMetrics.getDescent(); + } + + @Override + public int getHeight() { + return externalMetrics.getHeight(); + } + + @Override + public int getLeading() { + return externalMetrics.getLeading(); + } + + @Override + public int hashCode() { + return externalMetrics.hashCode(); + } + + @Override + public boolean equals(Object object) { + return externalMetrics.equals(object); + } + +} \ No newline at end of file diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/GCExtension.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/GCExtension.java new file mode 100644 index 00000000000..c971b7cae07 --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/GCExtension.java @@ -0,0 +1,534 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ + +package org.eclipse.swt.graphics; + +import java.util.*; + +import org.eclipse.swt.internal.canvasext.*; + +/** + * @noreference This class is not intended to be referenced by clients. + */ +public final class GCExtension extends GC { + + private final IExternalGC e; + + /** + * Blocks access to the static factory methods from GC. + * + * @noreference This class is not intended to be referenced by clients. + */ + public static GCExtension gtk_new(long handle, GCData data) { + throw new IllegalStateException("GCExtension does not support gtk_new"); + } + + /** + * Blocks access to the static factory methods from GC. + * + * @noreference This class is not intended to be referenced by clients. + */ + public static GCExtension gtk_new(Drawable drawable, GCData data) { + throw new IllegalStateException("GCExtension does not support gtk_new"); + } + + /** + * Blocks access to the GC constructor. + */ + GCExtension() { + throw new IllegalStateException("GCExtension does not supported protected constructor"); + } + + /** + * Blocks access to the GC constructor. + * + * @noreference This class is not intended to be referenced by clients. + */ + public GCExtension(Drawable drawable) { + throw new IllegalStateException("Invalid Constructor call"); + } + + /** + * Blocks access to the GC constructor. + * + * @noreference This class is not intended to be referenced by clients. + */ + public GCExtension(Drawable drawable, int style) { + throw new IllegalStateException("Invalid Constructor call"); + } + + /** + * Blocks access to the GC constructor. + * + * @noreference This class is not intended to be referenced by clients. + */ + public GCExtension(final IExternalGC ext) { + this.e = ext; + } + + @Override + public FontMetrics getFontMetrics() { + return e.getFontMetrics(); + } + + @Override + public void dispose() { + e.dispose(); + } + + @Override + public void drawImage(Image image, int destX, int destY, int destWidth, int destHeight) { + e.drawImage(image, destX, destY, destWidth, destHeight); + } + + @Override + public void copyArea(Image image, int x, int y) { + e.copyArea(image, x, y); + } + + @Override + public void copyArea(int srcX, int srcY, int width, int height, int destX, int destY) { + e.copyArea(srcX, srcY, width, height, destX, destY); + } + + @Override + public int getAdvanceWidth(char ch) { + return e.getAdvanceWidth(ch); + } + + @Override + public boolean getAdvanced() { + return e.getAdvanced(); + } + + @Override + public int getAlpha() { + return e.getAlpha(); + } + + @Override + public int getAntialias() { + return e.getAntialias(); + } + + @Override + public Color getBackground() { + return e.getBackground(); + } + + @Override + public Pattern getBackgroundPattern() { + return e.getBackgroundPattern(); + } + + @Override + public int getCharWidth(char ch) { + return e.getCharWidth(ch); + } + + @Override + public Rectangle getClipping() { + return e.getClipping(); + } + + @Override + public void getClipping(Region region) { + e.getClipping(region); + } + + @Override + public int getFillRule() { + return e.getFillRule(); + } + + @Override + public Font getFont() { + return e.getFont(); + } + + @Override + public Color getForeground() { + return e.getForeground(); + } + + @Override + public Pattern getForegroundPattern() { + return e.getForegroundPattern(); + } + + + /** + * @noreference This method is not intended to be referenced by clients. + */ + @Override + public GCData getGCData() { + return e.getGCData(); + } + + @Override + public int getInterpolation() { + return e.getInterpolation(); + } + + @Override + public LineAttributes getLineAttributes() { + return e.getLineAttributes(); + } + + @Override + public int getLineCap() { + return e.getLineCap(); + } + + @Override + public int[] getLineDash() { + return e.getLineDash(); + } + + @Override + public int getLineJoin() { + return e.getLineJoin(); + } + + @Override + public int getLineStyle() { + return e.getLineStyle(); + } + + @Override + public int getLineWidth() { + return e.getLineWidth(); + } + + @Override + public int getStyle() { + return e.getStyle(); + } + + @Override + public int getTextAntialias() { + return e.getTextAntialias(); + } + + @Override + public void getTransform(Transform transform) { + e.getTransform(transform); + } + + @Override + public boolean getXORMode() { + return e.getXORMode(); + } + + @Override + public boolean isClipped() { + return e.isClipped(); + } + + @Override + public boolean isDisposed() { + return e.isDisposed(); + } + + @Override + public void setAdvanced(boolean advanced) { + e.setAdvanced(advanced); + } + + @Override + public void setAlpha(int alpha) { + e.setAlpha(alpha); + } + + @Override + public void setAntialias(int antialias) { + e.setAntialias(antialias); + } + + @Override + public void setBackground(Color color) { + e.setBackground(color); + } + + @Override + public void setBackgroundPattern(Pattern pattern) { + e.setBackgroundPattern(pattern); + } + + @Override + public void setClipping(int x, int y, int width, int height) { + e.setClipping(x, y, width, height); + } + + @Override + public void setClipping(Path path) { + e.setClipping(path); + } + + @Override + public void setClipping(Rectangle rect) { + e.setClipping(rect); + } + + @Override + public void setClipping(Region region) { + e.setClipping(region); + } + + @Override + public void setFont(Font font) { + e.setFont(font); + } + + @Override + public void setFillRule(int rule) { + e.setFillRule(rule); + } + + @Override + public void setForeground(Color color) { + e.setForeground(color); + } + + @Override + public void setForegroundPattern(Pattern pattern) { + e.setForegroundPattern(pattern); + } + + @Override + public void setInterpolation(int interpolation) { + e.setInterpolation(interpolation); + } + + @Override + public void setLineAttributes(LineAttributes attributes) { + e.setLineAttributes(attributes); + } + + @Override + public void setLineCap(int cap) { + e.setLineCap(cap); + } + + @Override + public void setLineDash(int[] dashes) { + e.setLineDash(dashes); + } + + @Override + public void setLineJoin(int join) { + e.setLineJoin(join); + } + + @Override + public void setLineStyle(int lineStyle) { + e.setLineStyle(lineStyle); + } + + @Override + public void setLineWidth(int lineWidth) { + e.setLineWidth(lineWidth); + } + + @Override + public void setTextAntialias(int antialias) { + e.setTextAntialias(antialias); + } + + @Override + public void setTransform(Transform transform) { + e.setTransform(transform); + } + + @Override + public void setXORMode(boolean xor) { + e.setXORMode(xor); + } + + @Override + public Point stringExtent(String string) { + return e.stringExtent(string); + } + + @Override + public Point textExtent(String string) { + return e.textExtent(string); + } + + @Override + public Point textExtent(String string, int flags) { + return e.textExtent(string, flags); + } + + @Override + public void copyArea(int srcX, int srcY, int width, int height, int destX, int destY, boolean paint) { + e.copyArea(srcX, srcY, width, height, destX, destY, paint); + } + + @Override + public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) { + e.drawArc(x, y, width, height, startAngle, arcAngle); + } + + @Override + public void drawFocus(int x, int y, int width, int height) { + e.drawFocus(x, y, width, height); + } + + @Override + public void drawImage(Image image, int x, int y) { + e.drawImage(image, x, y); + } + + @Override + public void drawImage(Image image, int srcX, int srcY, int srcWidth, int srcHeight, int destX, int destY, + int destWidth, int destHeight) { + e.drawImage(image, srcX, srcY, srcWidth, srcHeight, destX, destY, destWidth, destHeight); + } + + @Override + public void drawLine(int x1, int y1, int x2, int y2) { + e.drawLine(x1, y1, x2, y2); + } + + @Override + public void drawOval(int x, int y, int width, int height) { + e.drawOval(x, y, width, height); + } + + @Override + public void drawPath(Path path) { + e.drawPath(path); + } + + @Override + public void drawPoint(int x, int y) { + e.drawPoint(x, y); + } + + @Override + public void drawPolygon(int[] pointArray) { + e.drawPolygon(pointArray); + } + + @Override + public void drawPolyline(int[] pointArray) { + e.drawPolyline(pointArray); + } + + @Override + public void drawRectangle(int x, int y, int width, int height) { + e.drawRectangle(x, y, width, height); + } + + @Override + public void drawRectangle(Rectangle rect) { + e.drawRectangle(rect); + } + + @Override + public void drawRoundRectangle(int x, int y, int width, int height, int arcWidth, int arcHeight) { + e.drawRoundRectangle(x, y, width, height, arcWidth, arcHeight); + } + + @Override + public void drawString(String string, int x, int y) { + e.drawString(string, x, y); + } + + @Override + public void drawString(String string, int x, int y, boolean isTransparent) { + e.drawString(string, x, y, isTransparent); + } + + @Override + public void drawText(String string, int x, int y) { + e.drawText(string, x, y); + } + + @Override + public void drawText(String string, int x, int y, boolean isTransparent) { + e.drawText(string, x, y, isTransparent); + } + + @Override + public void drawText(String string, int x, int y, int flags) { + e.drawText(string, x, y, flags); + } + + @Override + public boolean equals(Object object) { + return Objects.equals(object, this); + } + + @Override + public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) { + e.fillArc(x, y, width, height, startAngle, arcAngle); + } + + @Override + public void fillGradientRectangle(int x, int y, int width, int height, boolean vertical) { + e.fillGradientRectangle(x, y, width, height, vertical); + } + + @Override + public void fillOval(int x, int y, int width, int height) { + e.fillOval(x, y, width, height); + } + + @Override + public void fillPath(Path path) { + e.fillPath(path); + } + + @Override + public void fillPolygon(int[] pointArray) { + e.fillPolygon(pointArray); + } + + @Override + public void fillRectangle(int x, int y, int width, int height) { + e.fillRectangle(x, y, width, height); + } + + @Override + public void fillRectangle(Rectangle rect) { + e.fillRectangle(rect); + } + + @Override + public void fillRoundRectangle(int x, int y, int width, int height, int arcWidth, int arcHeight) { + e.fillRoundRectangle(x, y, width, height, arcWidth, arcHeight); + } + + @Override + public int hashCode() { + return e.hashCode(); + } + + @Override + public String toString() { + if (isDisposed()) + return "GCExtension {*DISPOSED*}"; + return "GCExtension {" + e + "}"; + } + + @Override + public Device getDevice() { + return e.getDevice(); + } + + public void textLayoutDraw(TextLayout textLayout, GC gc, int xInPoints, int yInPoints, int selectionStart, + int selectionEnd, Color selectionForeground, Color selectionBackground, int flags) { + e.textLayoutDraw(textLayout,gc,xInPoints,yInPoints,selectionStart, selectionEnd, selectionForeground, selectionBackground, flags); + } + +} \ No newline at end of file diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageVersion.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageVersion.java new file mode 100644 index 00000000000..46175eb0128 --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/ImageVersion.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * Contributors: + * SAP SE and others - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.graphics; + +/** + * @noreference This class is not intended to be referenced by clients. + */ +public class ImageVersion { + + private int version; + + public ImageVersion(int version) { + this.version = version; + } + + public static ImageVersion getVersion(Image image) { + return image.getImageVersion(); + } + + public int getVersion() { + return version; + } + +} diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/PatternProperties.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/PatternProperties.java new file mode 100644 index 00000000000..287a9b9848b --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/PatternProperties.java @@ -0,0 +1,95 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * Contributors: + * SAP SE and others - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.graphics; + +/** + * @noreference This class is not intended to be referenced by clients. + */ +public class PatternProperties { + private final Image image; + private float baseX1, baseY1, baseX2, baseY2; + private Color color1, color2; + private int alpha1, alpha2; + + public PatternProperties(Image image, float baseX1, float baseY1, float baseX2, float baseY2, Color color1, + int alpha1, Color color2, int alpha2) { + super(); + this.image = image; + this.baseX1 = baseX1; + this.baseY1 = baseY1; + this.baseX2 = baseX2; + this.baseY2 = baseY2; + this.color1 = color1; + this.color2 = color2; + this.alpha1 = alpha1; + this.alpha2 = alpha2; + } + + public PatternProperties(Image image) { + this.image = image; + } + + public PatternProperties(float baseX1, float baseY1, float baseX2, float baseY2, Color color1, int alpha1, + Color color2, int alpha2) { + super(); + this.image = null; + this.baseX1 = baseX1; + this.baseY1 = baseY1; + this.baseX2 = baseX2; + this.baseY2 = baseY2; + this.color1 = color1; + this.color2 = color2; + this.alpha1 = alpha1; + this.alpha2 = alpha2; + } + + public Image getImage() { + return image; + } + + public float getBaseX1() { + return baseX1; + } + + public float getBaseY1() { + return baseY1; + } + + public float getBaseX2() { + return baseX2; + } + + public float getBaseY2() { + return baseY2; + } + + public Color getColor1() { + return color1; + } + + public Color getColor2() { + return color2; + } + + public int getAlpha1() { + return alpha1; + } + + public int getAlpha2() { + return alpha2; + } + + public static PatternProperties get(Pattern pattern) { + return pattern.getProperties(); + } +} diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/RegionLog.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/RegionLog.java new file mode 100644 index 00000000000..0711e50f2a8 --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/graphics/RegionLog.java @@ -0,0 +1,49 @@ +package org.eclipse.swt.graphics; + +import java.util.*; + +/** + * @noreference This class is not intended to be referenced by clients. + */ +public final class RegionLog { + + public enum OpType { + ADD, INTERSECT, SUBTRACT, TRANSLATE + } + + public record Operation(OpType type, Object executionObject) { + } + + private final List op = new LinkedList<>(); + + + public void translate(Point p) { + op.add(new Operation(OpType.TRANSLATE, p)); + } + + public void intersect(Object ob) { + op.add(new Operation(OpType.INTERSECT, ob)); + } + + public void add(Object ob) { + op.add(new Operation(OpType.ADD, ob)); + } + + public void subtract(Object ob) { + op.add(new Operation(OpType.SUBTRACT, ob)); + } + + public List getOperations(){ + return op; + } + + public int getNumberOfOperations() { + return op.size(); + } + + public static RegionLog getLog(Region r) { + return r.getLog(); + } + + +} diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/canvasext/ExternalCanvasHandler.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/canvasext/ExternalCanvasHandler.java new file mode 100644 index 00000000000..abf5992c5fc --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/canvasext/ExternalCanvasHandler.java @@ -0,0 +1,154 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.swt.internal.canvasext; + +import java.util.*; + +import org.eclipse.swt.*; +import org.eclipse.swt.custom.*; +import org.eclipse.swt.widgets.*; + +/** + * Handles the creation of external canvas extensions based on the style of the + * canvas and the availability of an extension factory. + */ +public class ExternalCanvasHandler { + + private static boolean FAILED_WITH_ERRORS = false; + private static boolean ExternalCanvasWasLogged = false; + + // DISABLE the external canvas extension. This is necessary in case the + // extension causes problems on a platform. In this case, the user can set this + // property to disable the extension and use the default canvas implementation. + private final static String DISABLE_EXTERNAL_CANVAS = "org.eclipse.swt.external.canvas:disabled"; + + // This is only for test cases in order to check whether the software works with + // the canvas extension. + // NEVER USE THIS IN PRODUCTIVE CODE!! + private final static String FORCE_ENABLE_EXTERNAL_CANVAS = "org.eclipse.swt.external.canvas:forceEnabled"; + + // In order to know whether an external canvas was activated. + private final static String LOG_EXTERNAL_CANVAS_ACTIVATION = "org.eclipse.swt.external.canvas:logActivation"; + + private static IExternalCanvasFactory externalFactory = null; + private static boolean factoryLoaded = false; + + private static void loadFactory() { + + if (factoryLoaded) + return; + + factoryLoaded = true; + try { + // it is possible that the loading of external libraries fails. + externalFactory = ServiceLoader.load(IExternalCanvasFactory.class).findFirst().orElse(null); + } catch (Throwable t) { + FAILED_WITH_ERRORS = true; + t.printStackTrace(System.err); + } + } + + + private static boolean isDisabled() { + var disable = System.getProperty(DISABLE_EXTERNAL_CANVAS); + if (disable != null) { + return true; + } + return false; + } + + private static boolean isForcedEnabled() { + var forceEnable = System.getProperty(FORCE_ENABLE_EXTERNAL_CANVAS); + if (forceEnable != null) { + return true; + } + return false; + } + + private static boolean isLogActive() { + var log = System.getProperty(LOG_EXTERNAL_CANVAS_ACTIVATION); + if (log != null) { + return true; + } + return false; + } + + public static boolean isActive(Canvas canvas, int style) { + + if (FAILED_WITH_ERRORS) + return false; + + if (isDisabled()) { + + if (!ExternalCanvasWasLogged && isLogActive()) { + System.out.println("External canvas disabled."); + ExternalCanvasWasLogged = true; + } + + return false; + } + + if (canvas instanceof StyledText || canvas instanceof Decorations) + return false; + + loadFactory(); + + if ((style & SWT.SKIA) != 0 && externalFactory != null) + return true; + + if (externalFactory == null) { + if (!ExternalCanvasWasLogged && isLogActive()) { + System.out.println("No external canvas factory found. External canvas disabled."); + ExternalCanvasWasLogged = true; + } + return false; + } + + if (isForcedEnabled()) { + + if (!ExternalCanvasWasLogged && isLogActive()) { + System.out.println("Force enabled, external canvas factory found. External canvas will be activated."); + ExternalCanvasWasLogged = true; + } + + return true; + } + + return false; + } + + public static IExternalCanvasHandler createHandler(Canvas c) { + + if (FAILED_WITH_ERRORS) + return null; + + if (isDisabled()) + return null; + + loadFactory(); + + try { + // it is possible that the loading of external libraries fails. + var externalCanvas = externalFactory.createCanvasExtension(c); + + if (!ExternalCanvasWasLogged && isLogActive()) { + System.out.println("External canvas activated."); + ExternalCanvasWasLogged = true; + } + + return externalCanvas; + } catch (Throwable t) { + FAILED_WITH_ERRORS = true; + t.printStackTrace(System.err); + return null; + } + } +} diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/canvasext/IDpiScaler.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/canvasext/IDpiScaler.java new file mode 100644 index 00000000000..d37660ca409 --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/canvasext/IDpiScaler.java @@ -0,0 +1,6 @@ +package org.eclipse.swt.internal.canvasext; + +public interface IDpiScaler { + + int getNativeZoom(); +} diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/canvasext/IExternalCanvasFactory.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/canvasext/IExternalCanvasFactory.java new file mode 100644 index 00000000000..b9e71b0f5ef --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/canvasext/IExternalCanvasFactory.java @@ -0,0 +1,25 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.swt.internal.canvasext; + +import org.eclipse.swt.widgets.*; + +/** + * Defines a factory interface for creating external canvas handlers. + * Implementations of this interface can be provided to enable support for + * different types of canvas extensions based on the style of the canvas and the + * availability of an extension factory. + */ +public interface IExternalCanvasFactory { + + IExternalCanvasHandler createCanvasExtension(Canvas c); + +} diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/canvasext/IExternalCanvasHandler.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/canvasext/IExternalCanvasHandler.java new file mode 100644 index 00000000000..502b2abda6c --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/canvasext/IExternalCanvasHandler.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.swt.internal.canvasext; + +import java.util.function.*; + +import org.eclipse.swt.widgets.*; + +/** + * Interface with features necessary for external canvas handling. + */ +public interface IExternalCanvasHandler { + + Object paint(Consumer consumer, long wParam, long lParam); + + void redrawTriggered(); + + void redrawTriggered(int x, int y, int width, int height, boolean all); + + void scroll(int destX, int destY, int x, int y, int width, int height, boolean all); + +} diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/canvasext/IExternalFontMetrics.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/canvasext/IExternalFontMetrics.java new file mode 100644 index 00000000000..13431316131 --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/canvasext/IExternalFontMetrics.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * Contributors: + * SAP SE and others - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.internal.canvasext; + +public interface IExternalFontMetrics { + + public int getAscent(); + + public double getAverageCharacterWidth(); + + @Deprecated + public int getAverageCharWidth(); + + public int getDescent(); + + public int getHeight(); + + public int getLeading(); + +} \ No newline at end of file diff --git a/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/canvasext/IExternalGC.java b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/canvasext/IExternalGC.java new file mode 100644 index 00000000000..7b88a35f9a9 --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/common/org/eclipse/swt/internal/canvasext/IExternalGC.java @@ -0,0 +1,197 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * Contributors: + * SAP SE and others - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.internal.canvasext; + +import org.eclipse.swt.graphics.*; + +public interface IExternalGC { + + Device getDevice(); + + void copyArea(Image image, int i, int j); + + void dispose(); + + Point textExtent(String string); + + void setBackground(Color color); + + void setForeground(Color color); + + void fillRectangle(Rectangle rect); + + void drawImage(Image image, int x, int y); + + void drawImage(Image image, int srcX, int srcY, int srcWidth, int srcHeight, int destX, int destY, int destWidth, + int destHeight); + + void drawLine(int x1, int y1, int x2, int y2); + + Color getForeground(); + + void drawText(String string, int x, int y); + + void drawText(String string, int x, int y, boolean isTransparent); + + void drawText(String text, int x, int y, int flags); + + void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle); + + void drawFocus(int x, int y, int width, int height); + + void drawOval(int x, int y, int width, int height); + + void drawPath(Path path); + + void drawPoint(int x, int y); + + void drawPolygon(int[] pointArray); + + void drawPolyline(int[] pointArray); + + void drawRectangle(int x, int y, int width, int height); + + void drawRectangle(Rectangle rect); + + void drawRoundRectangle(int x, int y, int width, int height, int arcWidth, int arcHeight); + + void drawString(String string, int x, int y); + + void drawString(String string, int x, int y, boolean isTransparent); + + void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle); + + void fillGradientRectangle(int x, int y, int width, int height, boolean vertical); + + void fillOval(int x, int y, int width, int height); + + void fillPath(Path path); + + void fillPolygon(int[] pointArray); + + void fillRectangle(int x, int y, int width, int height); + + Point textExtent(String string, int flags); + + void fillRoundRectangle(int x, int y, int width, int height, int arcWidth, int arcHeight); + + void setFont(Font font); + + Font getFont(); + + void setAlpha(int alpha); + + int getAlpha(); + + void setLineWidth(int i); + + int getAntialias(); + + void setAntialias(int antialias); + + void setAdvanced(boolean enable); + + void setLineStyle(int lineStyle); + + int getLineStyle(); + + int getLineWidth(); + + LineAttributes getLineAttributes(); + + void setClipping(int x, int y, int width, int height); + + void setTransform(Transform transform); + + Point stringExtent(String string); + + int getLineCap(); + + Rectangle getClipping(); + + void copyArea(int srcX, int srcY, int width, int height, int destX, int destY); + + void copyArea(int srcX, int srcY, int width, int height, int destX, int destY, boolean paint); + + boolean isClipped(); + + int getFillRule(); + + void getClipping(Region region); + + int getAdvanceWidth(char ch); + + boolean getAdvanced(); + + int getCharWidth(char ch); + + Color getBackground(); + + Pattern getBackgroundPattern(); + + Pattern getForegroundPattern(); + + GCData getGCData(); + + int getInterpolation(); + + int[] getLineDash(); + + int getLineJoin(); + + int getStyle(); + + int getTextAntialias(); + + void getTransform(Transform transform); + + boolean getXORMode(); + + void setBackgroundPattern(Pattern pattern); + + void setClipping(Path path); + + void setClipping(Rectangle rect); + + void setClipping(Region region); + + void setFillRule(int rule); + + void setInterpolation(int interpolation); + + void setForegroundPattern(Pattern pattern); + + void setLineAttributes(LineAttributes attributes); + + void setLineCap(int cap); + + void setLineDash(int[] dashes); + + void setLineJoin(int join); + + void setXORMode(boolean xor); + + void setTextAntialias(int antialias); + + boolean isDisposed(); + + void drawImage(Image image, int destX, int destY, int destWidth, int destHeight); + + FontMetrics getFontMetrics(); + + Drawable getDrawable(); + + void textLayoutDraw(TextLayout textLayout, GC gc, int xInPoints, int yInPoints, int selectionStart, + int selectionEnd, Color selectionForeground, Color selectionBackground, int flags); + +} diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/FontMetrics.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/FontMetrics.java index 836325ca747..1fb0fc94108 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/FontMetrics.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/FontMetrics.java @@ -23,7 +23,7 @@ * @see GC#getFontMetrics * @see Sample code and further information */ -public final class FontMetrics { +public sealed class FontMetrics permits FontMetricsExtension { int ascentInPoints, descentInPoints, averageCharWidthInPoints; FontMetrics() { diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/FontProperties.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/FontProperties.java new file mode 100644 index 00000000000..63bc5a0a471 --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/FontProperties.java @@ -0,0 +1,57 @@ +package org.eclipse.swt.graphics; + +import org.eclipse.swt.*; +import org.eclipse.swt.internal.gtk.*; + +/** + * @noreference This class is not intended to be referenced by clients. + */ +public class FontProperties { + + public int lfHeight; + public int lfWidth = -1; + public int lfEscapement; + public int lfOrientation; + public int lfWeight = -1; + public byte lfItalic; + public byte lfUnderline; + public byte lfStrikeOut; + public String name; + + private FontProperties() { + + } + + /** + * @noreference This class is not intended to be referenced by clients. + */ + public static FontProperties getFontProperties(Font font) { + var fd = font.getFontData()[0]; + +// float height = (float)OS.pango_font_description_get_size(font.handle) / OS.PANGO_SCALE; + var stretch = OS.pango_font_description_get_stretch(font.handle); +// var variant = OS.pango_font_description_get_variant(font.handle); +// var style = OS.pango_font_description_get_style(font.handle); + var weight = OS.pango_font_description_get_weight(font.handle); + + var fp = new FontProperties(); + + fp.name = fd.getName(); + fp.lfHeight = fd.getHeight(); + + if((fd.getStyle() & SWT.ITALIC) != 0 ) + fp.lfItalic = 1; + fp.lfWeight = weight; + + fp.lfWidth = stretch + 1; + + + + return fp; + + } + + + + +} diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/GC.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/GC.java index 5d358be4b18..1802b6c17f5 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/GC.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/GC.java @@ -60,7 +60,7 @@ * @see SWT Examples: GraphicsExample, PaintExample * @see Sample code and further information */ -public final class GC extends Resource { +public sealed class GC extends Resource permits GCExtension { /** * the handle to the OS device context * (Warning: This field is platform dependent) @@ -2602,6 +2602,10 @@ void init(Drawable drawable, GCData data, long gdkGC) { if (cairoTransformationMatrix == null) cairoTransformationMatrix = new double[6]; Cairo.cairo_get_matrix(data.cairo, cairoTransformationMatrix); clipping = getClipping(); + if(this.drawable instanceof Image i) { + // it is likely that the GC modifies the image, so we increase the verion + i.increaseVersion(); + } } void initCairo() { diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/Image.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/Image.java index cbd3348943f..4692f9359fd 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/Image.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/Image.java @@ -173,6 +173,11 @@ public final class Image extends Resource implements Drawable { */ private int currentDeviceZoom = 100; + /** + * versions for an image cache + */ + private int version; + Image(Device device) { super(device); currentDeviceZoom = DPIUtil.getDeviceZoom(); @@ -1678,4 +1683,20 @@ public static void drawAtSize(GC gc, ImageData imageData, int width, int height) }); } +void increaseVersion() { + if(this.version == Integer.MAX_VALUE) { + this.version = 0; + } else { + this.version++; + } +} + +/** + * @noreference This field is not intended to be referenced by clients. + * @return The current version of the image, which is incremented each time the image data changes. + */ +public ImageVersion getImageVersion() { + return new ImageVersion(this.version); +} + } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/Region.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/Region.java index 929e6435e14..b34e96c059c 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/Region.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/Region.java @@ -46,6 +46,8 @@ public final class Region extends Resource { */ public long handle; + private final RegionLog log = new RegionLog(); + /** * Constructs a new empty region. *

@@ -93,6 +95,14 @@ public Region(Device device) { this.handle = handle; } +/** +* @noreference This method is not intended to be referenced by clients. +* @return a log characterizing the region +*/ +public RegionLog getLog() { + return log; +} + static long gdk_region_polygon(int[] pointArray, int npoints, int fill_rule) { //TODO this does not perform well and could fail if the polygon is too big int minX = pointArray[0], maxX = minX; @@ -161,6 +171,7 @@ public void add (int[] pointArray) { * with enough points for a polygon. */ if (pointArray.length < 6) return; + log.add(pointArray); long polyRgn = gdk_region_polygon(pointArray, pointArray.length / 2, GDK.GDK_EVEN_ODD_RULE); Cairo.cairo_region_union(handle, polyRgn); Cairo.cairo_region_destroy(polyRgn); @@ -207,6 +218,7 @@ public void add(Rectangle rect) { public void add(int x, int y, int width, int height) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); if (width < 0 || height < 0) SWT.error(SWT.ERROR_INVALID_ARGUMENT); + log.add(new Rectangle(x, y, width, height)); cairo_rectangle_int_t rect = new cairo_rectangle_int_t(); rect.x = x; rect.y = y; @@ -234,6 +246,7 @@ public void add(Region region) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); if (region == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); if (region.isDisposed()) SWT.error(SWT.ERROR_INVALID_ARGUMENT); + log.add(region); Cairo.cairo_region_union(handle, region.handle); } @@ -395,6 +408,7 @@ public void intersect(Rectangle rect) { public void intersect(int x, int y, int width, int height) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); if (width < 0 || height < 0) SWT.error(SWT.ERROR_INVALID_ARGUMENT); + log.intersect(new Rectangle(x,y,width,height)); cairo_rectangle_int_t rect = new cairo_rectangle_int_t(); rect.x = x; rect.y = y; @@ -426,6 +440,7 @@ public void intersect(Region region) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); if (region == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); if (region.isDisposed()) SWT.error(SWT.ERROR_INVALID_ARGUMENT); + log.intersect(region); Cairo.cairo_region_intersect(handle, region.handle); } @@ -532,6 +547,7 @@ public void subtract (int[] pointArray) { * with enough points for a polygon. */ if (pointArray.length < 6) return; + log.subtract(pointArray); long polyRgn = gdk_region_polygon(pointArray, pointArray.length / 2, GDK.GDK_EVEN_ODD_RULE); Cairo.cairo_region_subtract(handle, polyRgn); Cairo.cairo_region_destroy(polyRgn); @@ -579,6 +595,7 @@ public void subtract(Rectangle rect) { public void subtract(int x, int y, int width, int height) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); if (width < 0 || height < 0) SWT.error(SWT.ERROR_INVALID_ARGUMENT); + log.subtract(new Rectangle(x,y,width,height)); cairo_rectangle_int_t rect = new cairo_rectangle_int_t (); rect.x = x; rect.y = y; @@ -610,6 +627,7 @@ public void subtract(Region region) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); if (region == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); if (region.isDisposed()) SWT.error(SWT.ERROR_INVALID_ARGUMENT); + log.subtract(region); Cairo.cairo_region_subtract(handle, region.handle); } @@ -628,6 +646,7 @@ public void subtract(Region region) { */ public void translate (int x, int y) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); + log.translate(new Point(x,y)); Cairo.cairo_region_translate (handle, x, y); } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/TextLayout.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/TextLayout.java index 0cf16e2b58b..310d44d8ade 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/TextLayout.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/graphics/TextLayout.java @@ -619,6 +619,12 @@ public void draw(GC gc, int x, int y, int selectionStart, int selectionEnd, Colo } void drawInPixels(GC gc, int x, int y, int selectionStart, int selectionEnd, Color selectionForeground, Color selectionBackground, int flags) { checkLayout (); + + if(gc instanceof GCExtension gcext) { + gcext.textLayoutDraw(this, gc, x, y, selectionStart, selectionEnd, selectionForeground, selectionBackground, flags); + return; + } + computeRuns(); if (gc == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); if (gc.isDisposed()) SWT.error(SWT.ERROR_INVALID_ARGUMENT); diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/internal/canvasext/DpiScaler.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/internal/canvasext/DpiScaler.java new file mode 100644 index 00000000000..fbe5cffa8fd --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/internal/canvasext/DpiScaler.java @@ -0,0 +1,20 @@ +package org.eclipse.swt.internal.canvasext; + +import org.eclipse.swt.widgets.*; + +/** + * + * Provides utility methods for the canvas extension to scale values according + * to the current DPI settings of the OS. This is used internally to scale all + * values that are used for drawing and layout calculations. + */ +public class DpiScaler implements IDpiScaler { + + public DpiScaler(Canvas canvas) { + } + + public int getNativeZoom() { + return 100; + } + +} \ No newline at end of file diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/internal/canvasext/FontProperties.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/internal/canvasext/FontProperties.java new file mode 100644 index 00000000000..e1f1d19942a --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/internal/canvasext/FontProperties.java @@ -0,0 +1,74 @@ +package org.eclipse.swt.internal.canvasext; + +import java.util.*; + +import org.eclipse.swt.*; +import org.eclipse.swt.graphics.*; +import org.eclipse.swt.internal.gtk.*; + +public class FontProperties { + + public int lfHeight; + public int lfWidth = -1; + public int lfEscapement; + public int lfOrientation; + public int lfWeight = -1; + public byte lfItalic; + public byte lfUnderline; + public byte lfStrikeOut; + public String name; + + private FontProperties() { + + } + + @Override + public int hashCode() { + return Objects.hash(lfEscapement, lfHeight, lfItalic, lfOrientation, lfStrikeOut, lfUnderline, lfWeight, + lfWidth, name); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + FontProperties other = (FontProperties) obj; + return lfEscapement == other.lfEscapement && lfHeight == other.lfHeight && lfItalic == other.lfItalic + && lfOrientation == other.lfOrientation && lfStrikeOut == other.lfStrikeOut + && lfUnderline == other.lfUnderline && lfWeight == other.lfWeight && lfWidth == other.lfWidth + && Objects.equals(name, other.name); + } + + public static FontProperties getFontProperties(Font font) { + var fd = font.getFontData()[0]; + +// float height = (float)OS.pango_font_description_get_size(font.handle) / OS.PANGO_SCALE; + var stretch = OS.pango_font_description_get_stretch(font.handle); +// var variant = OS.pango_font_description_get_variant(font.handle); +// var style = OS.pango_font_description_get_style(font.handle); + var weight = OS.pango_font_description_get_weight(font.handle); + + var fp = new FontProperties(); + + fp.name = fd.getName(); + fp.lfHeight = fd.getHeight(); + + if((fd.getStyle() & SWT.ITALIC) != 0 ) + fp.lfItalic = 1; + fp.lfWeight = weight; + + fp.lfWidth = stretch + 1; + + + + return fp; + + } + + + +} diff --git a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Canvas.java b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Canvas.java index 250818d83b6..fcd72d91b52 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Canvas.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/gtk/org/eclipse/swt/widgets/Canvas.java @@ -17,6 +17,7 @@ import org.eclipse.swt.*; import org.eclipse.swt.graphics.*; import org.eclipse.swt.internal.cairo.*; +import org.eclipse.swt.internal.canvasext.*; import org.eclipse.swt.internal.gtk.*; /** @@ -45,6 +46,7 @@ public class Canvas extends Composite { Caret caret; IME ime; boolean blink, drawFlag; + private IExternalCanvasHandler externalCanvasHandler; Canvas () {} @@ -76,6 +78,28 @@ public class Canvas extends Composite { */ public Canvas (Composite parent, int style) { super (parent, checkStyle (style)); + if( ExternalCanvasHandler.isActive(this,style)) + externalCanvasHandler = ExternalCanvasHandler.createHandler(this); +} + +@Override +public void redraw () { + + if(externalCanvasHandler != null) { + externalCanvasHandler.redrawTriggered(); + } + super.redraw(); +} + +@Override +public void redraw (int x, int y, int width, int height, boolean all) { + if(externalCanvasHandler != null) { + externalCanvasHandler.redrawTriggered(x,y,width,height,all); + super.redraw(); + } + else { + super.redraw(x,y,width,height,all); + } } /** @@ -170,6 +194,12 @@ long gtk_commit (long imcontext, long text) { @Override long gtk_draw (long widget, long cairo) { if ((state & OBSCURED) != 0) return 0; + + if(externalCanvasHandler != null) { + externalCanvasHandler.paint((e)-> sendEvent(SWT.Paint, e),widget, cairo); + return 0; + } + long result = super.gtk_draw (widget, cairo); drawCaretInFocus(cairo); return result; @@ -331,6 +361,10 @@ void reskinChildren (int flags) { * */ public void scroll (int destX, int destY, int x, int y, int width, int height, boolean all) { + if(this.externalCanvasHandler != null) { + this.externalCanvasHandler.scroll(destX, destY, x, y, width, height, all); + return; + } checkWidget(); if (width <= 0 || height <= 0) return; /* diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/FontMetrics.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/FontMetrics.java index 457d3d8f029..01cfe312799 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/FontMetrics.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/FontMetrics.java @@ -27,7 +27,7 @@ * @see GC#getFontMetrics * @see Sample code and further information */ -public final class FontMetrics { +public sealed class FontMetrics permits FontMetricsExtension { /** * On Windows, handle is a Win32 TEXTMETRIC struct diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java index 85a110bb40d..c59eb0026dd 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/GC.java @@ -63,7 +63,7 @@ * @see SWT Examples: GraphicsExample, PaintExample * @see Sample code and further information */ -public final class GC extends Resource { +public sealed class GC extends Resource permits GCExtension { /** * the handle to the OS device context @@ -4555,6 +4555,11 @@ private void init(Drawable drawable, GCData data, long hDC, ImageHandle imageHan this.drawable = drawable; this.data = data; handle = hDC; + + if(this.drawable instanceof Image i) { + // it is likely that the GC modifies the image, so we increase the verion + i.increaseVersion(); + } } private static int extractZoom(long hDC) { diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Image.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Image.java index eed02b72093..7816a358911 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Image.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Image.java @@ -261,6 +261,8 @@ private InternalImageHandle getOrCreateImageHandleAtClosestSize(int widthHint, i private final HandleAtSize lastRequestedHandle = new HandleAtSize(); + private int version; + private Image (Device device, int type, long handle, int nativeZoom) { super(device); this.type = type; @@ -3321,4 +3323,20 @@ void destroy() { } } + + void increaseVersion() { + if(this.version == Integer.MAX_VALUE) { + this.version = 0; + } else { + this.version++; + } + } + + /** + * @noreference This field is not intended to be referenced by clients. + * @return The current version of the image, which is incremented each time the image data changes. + */ + public ImageVersion getImageVersion() { + return new ImageVersion(this.version); + } } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Pattern.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Pattern.java index 5afccb2f3b2..198cdccb36c 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Pattern.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Pattern.java @@ -219,6 +219,14 @@ void destroyHandlesExcept(Set zoomLevels) { }); } +/** + * Package-protected for internal use by fragments. + * @return the properties of this pattern, which can be used to create a copy of the pattern with the same properties as this pattern + */ +PatternProperties getProperties() { + return new PatternProperties(image, baseX1, baseY1, baseX2, baseY2, color1, alpha1, color2, alpha2); +} + Pattern copy() { if (image != null) { return new Pattern(device, image); @@ -364,4 +372,4 @@ protected void destroy() { } } } -} +} \ No newline at end of file diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Region.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Region.java index 8aa648f3787..15fb1b06c15 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Region.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/Region.java @@ -37,6 +37,8 @@ */ public final class Region extends Resource { + private final RegionLog log = new RegionLog(); + private Map zoomToHandle = new HashMap<>(); private List operations = new ArrayList<>(); @@ -104,6 +106,7 @@ public Region (Device device) { public void add (int[] pointArray) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); if (pointArray == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + log.add(pointArray); final Operation operation = new OperationWithArray(Operation::add, Arrays.copyOf(pointArray, pointArray.length)); storeAndApplyOperationForAllHandles(operation); } @@ -125,6 +128,7 @@ public void add (int[] pointArray) { public void add (Rectangle rect) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); if (rect == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + log.add(rect); final Operation operation = new OperationWithRectangle(Operation::add, new Rectangle(rect.x, rect.y, rect.width, rect.height)); storeAndApplyOperationForAllHandles(operation); } @@ -149,6 +153,7 @@ public void add (Rectangle rect) { */ public void add (int x, int y, int width, int height) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); + log.add(new Rectangle(x,y,width,height)); final Operation operation = new OperationWithRectangle(Operation::add, new Rectangle(x, y, width, height)); storeAndApplyOperationForAllHandles(operation); } @@ -172,6 +177,7 @@ public void add (Region region) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); if (region == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); if (region.isDisposed()) SWT.error(SWT.ERROR_INVALID_ARGUMENT); + log.add(region); if (!region.operations.isEmpty()) { adoptTemporaryHandleZoomHint(region); final Operation operation = new OperationWithRegion(Operation::add, region.operations); @@ -185,6 +191,14 @@ private void adoptTemporaryHandleZoomHint(Region region) { } } +/** +* @return a log characterizing the region +*/ +RegionLog getLog() { + return log; +} + + /** * Returns true if the point specified by the * arguments is inside the area specified by the receiver, @@ -332,6 +346,7 @@ public int hashCode () { public void intersect (Rectangle rect) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); if (rect == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + log.intersect(rect); final Operation operation = new OperationWithRectangle(Operation::intersect, new Rectangle(rect.x, rect.y, rect.width, rect.height)); storeAndApplyOperationForAllHandles(operation); } @@ -356,6 +371,7 @@ public void intersect (Rectangle rect) { */ public void intersect (int x, int y, int width, int height) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); + log.intersect(new Rectangle(x,y,width,height)); final Operation operation = new OperationWithRectangle(Operation::intersect, new Rectangle(x, y, width, height)); storeAndApplyOperationForAllHandles(operation); } @@ -381,6 +397,7 @@ public void intersect (Region region) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); if (region == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); if (region.isDisposed()) SWT.error(SWT.ERROR_INVALID_ARGUMENT); + log.intersect(region); if (!region.operations.isEmpty()) { adoptTemporaryHandleZoomHint(region); final Operation operation = new OperationWithRegion(Operation::intersect, region.operations); @@ -511,6 +528,7 @@ void set(Function handleForZoomSupplier, int contextZoom) { public void subtract (int[] pointArray) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); if (pointArray == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + log.subtract(pointArray); final Operation operation = new OperationWithArray(Operation::subtract, Arrays.copyOf(pointArray, pointArray.length)); storeAndApplyOperationForAllHandles(operation); } @@ -534,6 +552,7 @@ public void subtract (int[] pointArray) { public void subtract (Rectangle rect) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); if (rect == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + log.subtract(rect); final Operation operation = new OperationWithRectangle(Operation::subtract, new Rectangle(rect.x, rect.y, rect.width, rect.height)); storeAndApplyOperationForAllHandles(operation); } @@ -558,6 +577,7 @@ public void subtract (Rectangle rect) { */ public void subtract (int x, int y, int width, int height) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); + log.subtract(new Rectangle(x,y,width,height)); final Operation operation = new OperationWithRectangle(Operation::subtract, new Rectangle(x, y, width, height)); storeAndApplyOperationForAllHandles(operation); } @@ -583,6 +603,7 @@ public void subtract (Region region) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); if (region == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); if (region.isDisposed()) SWT.error(SWT.ERROR_INVALID_ARGUMENT); + log.subtract(region); if (!region.operations.isEmpty()) { adoptTemporaryHandleZoomHint(region); final Operation operation = new OperationWithRegion(Operation::subtract, region.operations); @@ -605,6 +626,7 @@ public void subtract (Region region) { */ public void translate (int x, int y) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); + log.translate(new Point(x,y)); final Operation operation = new OperationWithPoint(Operation::translate, new Point(x, y)); storeAndApplyOperationForAllHandles(operation); } @@ -627,6 +649,7 @@ public void translate (int x, int y) { public void translate (Point pt) { if (isDisposed()) SWT.error(SWT.ERROR_GRAPHIC_DISPOSED); if (pt == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); + log.translate(pt); final Operation operation = new OperationWithPoint(Operation::translate, new Point(pt.x, pt.y)); storeAndApplyOperationForAllHandles(operation); } diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/TextLayout.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/TextLayout.java index 0dd41a0f02b..5b32dd4ab67 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/TextLayout.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/graphics/TextLayout.java @@ -706,6 +706,9 @@ long createGdipBrush(Color color, int alpha) { public void draw (GC gc, int x, int y) { checkLayout(); drawInPixels(gc, x, y); + + + } /** @@ -788,6 +791,12 @@ void drawInPixels (GC gc, int xInPoints, int yInPoints) { } void drawInPixels (GC gc, int xInPoints, int yInPoints, int selectionStart, int selectionEnd, Color selectionForeground, Color selectionBackground, int flags) { + + if(gc instanceof GCExtension gcext) { + gcext.textLayoutDraw(this, gc, xInPoints, yInPoints, selectionStart, selectionEnd, selectionForeground, selectionBackground, flags); + return; + } + computeRuns(gc); if (gc == null) SWT.error(SWT.ERROR_NULL_ARGUMENT); if (gc.isDisposed()) SWT.error(SWT.ERROR_INVALID_ARGUMENT); @@ -4029,4 +4038,5 @@ int untranslateOffset(int offset) { public void setDefaultTabWidth(int tabLength) { // unused in win32 } + } \ No newline at end of file diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/internal/canvasext/DpiScaler.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/internal/canvasext/DpiScaler.java new file mode 100644 index 00000000000..7681a9d2913 --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/internal/canvasext/DpiScaler.java @@ -0,0 +1,36 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * Contributors: + * SAP SE and others - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.internal.canvasext; + +import org.eclipse.swt.widgets.*; + +/** + * + * Provides utility methods for the canvas extension to scale values according + * to the current DPI settings of the OS. This is used internally to scale all + * values that are used for drawing and layout calculations. + */ +public class DpiScaler implements IDpiScaler { + + private Canvas canvas; + + public DpiScaler(Canvas canvas) { + this.canvas = canvas; + } + + @Override + public int getNativeZoom() { + return canvas.nativeZoom; + } + +} \ No newline at end of file diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/internal/canvasext/FontProperties.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/internal/canvasext/FontProperties.java new file mode 100644 index 00000000000..c32ccb3015b --- /dev/null +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/internal/canvasext/FontProperties.java @@ -0,0 +1,151 @@ +/******************************************************************************* + * Copyright (c) 2025 SAP SE and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * Contributors: + * SAP SE and others - initial API and implementation + *******************************************************************************/ +package org.eclipse.swt.internal.canvasext; + +import java.util.*; + +import org.eclipse.swt.graphics.*; + +/** + * Provides extended information about a font, including its stretch factor and other properties. + */ +public class FontProperties { + + // Standard OpenType stretch values + private static final Map STRETCH_MAP = new HashMap<>(); + private static final Set REGION_KEYS = new HashSet<>(); + + static { + STRETCH_MAP.put("UltraCondensed", 1); // 50.0% + STRETCH_MAP.put("ExtraCondensed", 2); // 62.5% + STRETCH_MAP.put("Condensed", 3); // 75.0% + STRETCH_MAP.put("SemiCondensed", 4); // 87.5% + STRETCH_MAP.put("Normal", 5); // 100.0% + STRETCH_MAP.put("Medium", 5); + STRETCH_MAP.put("SemiExpanded", 6); // 112.5% + STRETCH_MAP.put("Expanded", 7); // 125.0% + STRETCH_MAP.put("ExtraExpanded", 8); // 150.0% + STRETCH_MAP.put("UltraExpanded", 9); // 200.0% + + + REGION_KEYS.add(" Greek"); + REGION_KEYS.add(" TUR"); + REGION_KEYS.add(" Baltic"); + REGION_KEYS.add(" CE"); + REGION_KEYS.add(" CYR"); + REGION_KEYS.add(" Transparent"); + } + + + + + + public int lfHeight; + public int lfWidth; + public int lfEscapement; + public int lfOrientation; + public int lfWeight; + public byte lfItalic; + public byte lfUnderline; + public byte lfStrikeOut; + public String name; + + private FontProperties() { + + } + + private static FontProperties getFontProperties(FontData fd) { + var fp = new FontProperties(); + var d = fd.data; + + String name = fd.getName(); + + for(String local : REGION_KEYS) { + if(name.endsWith(local)) { + name = name.substring(0, name.length() - local.length()); + break; + } + + } + + var fontName = analyzeManual(name); + + fp.name = fontName.baseName; + fp.lfHeight = fd.getHeight(); + fp.lfItalic = d.lfItalic; + fp.lfEscapement = d.lfEscapement; + fp.lfOrientation = d.lfOrientation; + fp.lfStrikeOut = d.lfStrikeOut; + fp.lfUnderline = d.lfUnderline; + if(d.lfWeight == 0) + fp.lfWeight = 400; // Normal weight + else + fp.lfWeight = d.lfWeight; + var stretch = STRETCH_MAP.get(fontName.detectedStretch); + if(stretch != null) + fp.lfWidth = stretch; + + return fp; + } + + public static FontProperties getFontProperties(Font font) { + return getFontProperties(font.getFontData()[0]); + } + + private static FontName analyzeManual(String fullDescription) { + // known stretch keywords + String[] stretchKeywords = { "UltraCondensed", "ExtraCondensed", "Condensed", "SemiCondensed", "SemiExpanded", + "Expanded" }; + + String baseName = fullDescription; + String detectedStretch = null; + + for (String keyword : stretchKeywords) { + if (fullDescription.contains(keyword)) { + detectedStretch = keyword; + baseName = fullDescription; + break; + } + } + + return new FontName(baseName, detectedStretch); + } + + private record FontName(String baseName, String detectedStretch) { + } + + @Override + public int hashCode() { + return Objects.hash(lfEscapement, lfHeight, lfItalic, lfOrientation, lfStrikeOut, lfUnderline, lfWeight, + lfWidth, name); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + FontProperties other = (FontProperties) obj; + return lfEscapement == other.lfEscapement && lfHeight == other.lfHeight && lfItalic == other.lfItalic + && lfOrientation == other.lfOrientation && lfStrikeOut == other.lfStrikeOut + && lfUnderline == other.lfUnderline && lfWeight == other.lfWeight && lfWidth == other.lfWidth + && Objects.equals(name, other.name); + } + + + + +} diff --git a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Canvas.java b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Canvas.java index cf4df92bc54..09397611df4 100644 --- a/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Canvas.java +++ b/bundles/org.eclipse.swt/Eclipse SWT/win32/org/eclipse/swt/widgets/Canvas.java @@ -17,6 +17,7 @@ import org.eclipse.swt.*; import org.eclipse.swt.graphics.*; import org.eclipse.swt.internal.*; +import org.eclipse.swt.internal.canvasext.*; import org.eclipse.swt.internal.win32.*; /** @@ -44,6 +45,7 @@ public class Canvas extends Composite { Caret caret; IME ime; + private IExternalCanvasHandler externalCanvasHandler; /** * Prevents uninitialized instances from being created outside the package. @@ -79,6 +81,50 @@ public class Canvas extends Composite { */ public Canvas (Composite parent, int style) { super (parent, style); + if( ExternalCanvasHandler.isActive(this,style)) + externalCanvasHandler = ExternalCanvasHandler.createHandler(this); +} + +@Override +public void redraw () { + + if(externalCanvasHandler != null) { + externalCanvasHandler.redrawTriggered(); + } + super.redraw(); +} + +@Override +public void redraw (int x, int y, int width, int height, boolean all) { + + if(externalCanvasHandler != null) { + externalCanvasHandler.redrawTriggered( x, y, width, height, all); + super.redraw(); + } + else + super.redraw( x, y, width, height, all); +} + +@Override +LRESULT WM_PAINT (long wParam, long lParam) { + if(externalCanvasHandler != null) + { + + if ((state & DISPOSE_SENT) != 0) return LRESULT.ZERO; + + var result = externalCanvasHandler.paint(e -> sendEvent(SWT.Paint,e),wParam, lParam); + if(result instanceof Integer i) + return new LRESULT(i); + + if(result instanceof LRESULT r) { + return r; + } + return null; + + } + + return super.WM_PAINT(wParam, lParam); + } /** @@ -196,7 +242,12 @@ void reskinChildren (int flags) { * */ public void scroll (int destX, int destY, int x, int y, int width, int height, boolean all) { + if(this.externalCanvasHandler != null) { + this.externalCanvasHandler.scroll(destX, destY, x, y, width, height, all); + return; + } checkWidget (); + int zoom = getAutoscalingZoom(); destX = DPIUtil.pointToPixel(destX, zoom); destY = DPIUtil.pointToPixel(destY, zoom); diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/graphics/GraphicsExample.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/graphics/GraphicsExample.java index 67477fe2a21..58d30f9b422 100644 --- a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/graphics/GraphicsExample.java +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/graphics/GraphicsExample.java @@ -70,7 +70,7 @@ public class GraphicsExample { Sash hSash, vSash; Canvas canvas; Composite tabControlPanel; - ToolItem backItem, dbItem; // background, double buffer items + ToolItem backItem, dbItem,skiaItem; // background, double buffer, skia items Menu backMenu; // background menu item List resources; // stores resources that will be disposed @@ -205,6 +205,7 @@ void createControls(final Composite parent) { void createCanvas(Composite parent) { int style = SWT.NO_BACKGROUND; if (dbItem.getSelection()) style |= SWT.DOUBLE_BUFFERED; + if(skiaItem.getSelection()) style |= SWT.SKIA; canvas = new Canvas(parent, style); canvas.addListener(SWT.Paint, event -> { GC gc = event.gc; @@ -233,7 +234,11 @@ void createCanvas(Composite parent) { } void recreateCanvas() { - if (dbItem.getSelection() == ((canvas.getStyle() & SWT.DOUBLE_BUFFERED) != 0)) return; + boolean recreate = false; + if (dbItem.getSelection() != ((canvas.getStyle() & SWT.DOUBLE_BUFFERED) != 0)) recreate |= true; + if (skiaItem.getSelection() != ((canvas.getStyle() & SWT.SKIA) != 0)) recreate |= true; + if(recreate == false) return; + Object data = canvas.getLayoutData(); if (canvas != null) canvas.dispose(); createCanvas(parent); @@ -312,6 +317,14 @@ void createToolBar(final Composite parent) { dbItem.setText(getResourceString("DoubleBuffer")); //$NON-NLS-1$ dbItem.setImage(loadImage(display, "db.gif")); //$NON-NLS-1$ dbItem.addListener(SWT.Selection, event -> setDoubleBuffered(dbItem.getSelection())); + + // skia tool item + skiaItem = new ToolItem(toolBar, SWT.CHECK); + skiaItem.setText(getResourceString("Skia")); //$NON-NLS-1$ + skiaItem.setImage(loadImage(display, "db.gif")); //$NON-NLS-1$ + skiaItem.setSelection(true); + skiaItem.addListener(SWT.Selection, event -> setSkia(skiaItem.getSelection())); + } /** @@ -595,6 +608,14 @@ public void setDoubleBuffered(boolean doubleBuffered) { recreateCanvas(); } +/** + * Sets whether the canvas is double buffered or not. + */ +public void setSkia(boolean skia) { + skiaItem.setSelection(skia); + recreateCanvas(); +} + /** * Grabs input focus. */ diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/graphics/LineStyleTab.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/graphics/LineStyleTab.java index a14f67a6654..f9057fdc8b9 100644 --- a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/graphics/LineStyleTab.java +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/graphics/LineStyleTab.java @@ -116,6 +116,9 @@ public void createControlPanel(Composite parent) { public void paint(GC gc, int width, int height) { Device device = gc.getDevice(); + int sections = 7; + int textPositions = 14; + Pattern pattern = null; if (lineColor.getBgColor1() != null) { gc.setForeground(lineColor.getBgColor1()); @@ -128,15 +131,18 @@ public void paint(GC gc, int width, int height) { gc.setLineWidth(lineWidthSpinner.getSelection()); // draw lines with caps - gc.drawLine(3*width/16, 1*height/6, 13*width/16, 1*height/6); + gc.drawLine(3*width/16, 1*height/sections, 13*width/16, 1*height/sections); gc.setLineStyle(SWT.LINE_DASH); - gc.drawLine(3*width/16, 2*height/6, 13*width/16, 2*height/6); + gc.drawLine(3*width/16, 2*height/sections, 13*width/16, 2*height/sections); gc.setLineStyle(SWT.LINE_DOT); - gc.drawLine(3*width/16, 3*height/6, 13*width/16, 3*height/6); + gc.drawLine(3*width/16, 3*height/sections, 13*width/16, 3*height/sections); gc.setLineStyle(SWT.LINE_DASHDOT); - gc.drawLine(3*width/16, 4*height/6, 13*width/16, 4*height/6); + gc.drawLine(3*width/16, 4*height/sections, 13*width/16, 4*height/sections); gc.setLineStyle(SWT.LINE_DASHDOTDOT); - gc.drawLine(3*width/16, 5*height/6, 13*width/16, 5*height/6); + gc.drawLine(3*width/16, 5*height/sections, 13*width/16, 5*height/sections); + gc.setLineDash(new int[] {25,20,15,10,5}); + gc.drawLine(3*width/16, 6*height/sections, 13*width/16, 6*height/sections); + // draw labels Font font = new Font(device, getPlatformFont(), 20, SWT.NORMAL); @@ -146,19 +152,22 @@ public void paint(GC gc, int width, int height) { String text = GraphicsExample.getResourceString("Solid"); //$NON-NLS-1$ Point size = gc.stringExtent(text); - gc.drawString(text, (width-size.x)/2, 1*height/12, true); + gc.drawString(text, (width-size.x)/2, 1*height/textPositions, true); text = GraphicsExample.getResourceString("Dash"); //$NON-NLS-1$ size = gc.stringExtent(text); - gc.drawString(text, (width-size.x)/2, 3*height/12, true); + gc.drawString(text, (width-size.x)/2, 3*height/textPositions, true); text = GraphicsExample.getResourceString("Dot"); //$NON-NLS-1$ size = gc.stringExtent(text); - gc.drawString(text, (width-size.x)/2, 5*height/12, true); + gc.drawString(text, (width-size.x)/2, 5*height/textPositions, true); text = GraphicsExample.getResourceString("DashDot"); //$NON-NLS-1$ size = gc.stringExtent(text); - gc.drawString(text, (width-size.x)/2, 7*height/12, true); + gc.drawString(text, (width-size.x)/2, 7*height/textPositions, true); text = GraphicsExample.getResourceString("DashDotDot"); //$NON-NLS-1$ size = gc.stringExtent(text); - gc.drawString(text, (width-size.x)/2, 9*height/12, true); + gc.drawString(text, (width-size.x)/2, 9*height/textPositions, true); + text = GraphicsExample.getResourceString("CustomLineStyle {25,20,15,10,5}"); //$NON-NLS-1$ + size = gc.stringExtent(text); + gc.drawString(text, (width-size.x)/2, 11*height/textPositions , true); font.dispose(); } diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetAlpha.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetAlpha.java new file mode 100644 index 00000000000..e61c5cfee7d --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetAlpha.java @@ -0,0 +1,63 @@ +package org.eclipse.swt.examples.skia; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.RGBA; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Shell; + +public class SnippetAlpha { + + final static boolean USE_SKIA = true; // Set to true to use Skia rendering, false for default SWT rendering + + public static void main(String[] args) { + final Display display = new Display(); + final Shell shell = new Shell(display); + shell.setText("Snippet Alpha"); + + var style = SWT.DOUBLE_BUFFERED | (USE_SKIA ? SWT.SKIA : SWT.NONE); + + final Canvas canvas = new Canvas(shell,style); + canvas.setSize(400, 400); + shell.setSize(420, 440); + + canvas.addListener(SWT.Paint, SnippetAlpha::onPaint); + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + display.dispose(); + } + + private static void onPaint(Event e) { + + e.gc.setAdvanced(false); + e.gc.setBackground(e.display.getSystemColor(SWT.COLOR_RED)); + e.gc.fillRectangle(0, 0, 100, 100); + + // on windows and linux alpha in colors is not supported at all + Color c = new Color(e.display, new RGBA(0, 0, 255, 50)); + e.gc.setBackground(c); + e.gc.fillRectangle(200, 0, 100, 100); + c.dispose(); + + // on windows and linux alpha colors is not supported at all + + e.gc.setAlpha(100); + e.gc.setBackground(e.display.getSystemColor(SWT.COLOR_GREEN)); + e.gc.fillRectangle(0, 200, 100, 100); + c.dispose(); + + + e.gc.setLineWidth(10); + e.gc.setForeground(e.display.getSystemColor(SWT.COLOR_YELLOW)); + e.gc.drawLine(0, 0, 500, 500); + + + + } +} \ No newline at end of file diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetAnimatedProgress.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetAnimatedProgress.java new file mode 100644 index 00000000000..1d0e12e4f2c --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetAnimatedProgress.java @@ -0,0 +1,49 @@ +package org.eclipse.swt.examples.skia; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.AnimatedProgress; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +/** + * Example usage of AnimatedProgress (deprecated). + */ +public class SnippetAnimatedProgress { + + final static boolean USE_SKIA = true; // Set to true to use Skia rendering, false for default SWT rendering + + + @SuppressWarnings("deprecation") + public static void main(String[] args) { + Display display = new Display(); + Shell shell = new Shell(display); + shell.setText("AnimatedProgress Example"); + shell.setLayout(new GridLayout(1, false)); + + Composite composite = new Composite(shell, SWT.NONE); + composite.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + composite.setLayout(new GridLayout(1, false)); + + AnimatedProgress progress = new AnimatedProgress(composite, SWT.HORIZONTAL | SWT.BORDER | (USE_SKIA ? SWT.SKIA : SWT.NONE)); + progress.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + Button startButton = new Button(composite, SWT.PUSH); + startButton.setText("Start Animation"); + startButton.addListener(SWT.Selection, e -> progress.start()); + + Button stopButton = new Button(composite, SWT.PUSH); + stopButton.setText("Stop Animation"); + stopButton.addListener(SWT.Selection, e -> progress.stop()); + + shell.setSize(300, 120); + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) display.sleep(); + } + display.dispose(); + } +} diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetBalls.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetBalls.java new file mode 100644 index 00000000000..0bff43ada6c --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetBalls.java @@ -0,0 +1,204 @@ +package org.eclipse.swt.examples.skia; + +import java.util.LinkedList; + +/* + * Fill a shape with a predefined pattern + * + * For a list of all SWT example snippets see + * http://www.eclipse.org/swt/snippets/ + * + * @since 3.1 + */ +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Device; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Path; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.graphics.Transform; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +public class SnippetBalls { + + final static boolean USE_SKIA = true; + final static int BALL_COUNT = 3; + + static int skiaStyle = USE_SKIA ? SWT.SKIA : SWT.NONE; + final static int WIDTH = 100; + final static int HEIGHT = 100; + static final int TIMER = 30; + + static Composite parent; + static Canvas canvas; + static boolean animate = true; + + public static void main(String[] args) { + + Display display = new Display(); + bc = new BallCollection[BALL_COUNT]; + + Shell shell = new Shell(display); + shell.setText("SnippetBalls"); + shell.setLayout(new FillLayout()); + parent = shell; + Canvas c = new Canvas(shell, SWT.DOUBLE_BUFFERED | skiaStyle); + canvas = c; + + startAnimationTimer(); + c.setBackground(display.getSystemColor(SWT.COLOR_GREEN)); + c.addListener(SWT.Paint, event -> { + Rectangle r = ((Composite) event.widget).getClientArea(); + GC gc1 = event.gc; + + paint(gc1, r.width, r.height); + + }); + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + animate = false; + + display.dispose(); + } + + static BallCollection[] bc; + + /** + * This inner class serves as a container for the data needed to display a + * collection of balls. + */ + static class BallCollection { + float x, y; // position of ball + float incX, incY; // values by which to move the ball + int ball_size; // size (diameter) of the ball + int capacity; // number of balls in the collection + LinkedList prevx, prevy; // collection of previous x and y positions + // of ball + Color[] colors; // colors used for this ball collection + + public BallCollection(float x, float y, float incX, float incY, int ball_size, int capacity, Color[] colors) { + this.x = x; + this.y = y; + this.incX = incX; + this.incY = incY; + this.ball_size = ball_size; + this.capacity = capacity; + prevx = new LinkedList<>(); + prevy = new LinkedList<>(); + this.colors = colors; + } + } + + /** + * Starts the animation if the animate flag is set. + */ + static void startAnimationTimer() { + final Display display = parent.getDisplay(); + display.timerExec(TIMER, new Runnable() { + @Override + public void run() { + if (canvas.isDisposed()) + return; + int timeout = TIMER; + if (animate) { + Rectangle rect = canvas.getClientArea(); + next(rect.width, rect.height); + canvas.redraw(); + canvas.update(); + display.timerExec(timeout, this); + } + } + }); + } + + public int getInitialAnimationTime() { + return 10; + } + + public static void next(int width, int height) { + for (int i = 0; i < bc.length; i++) { + if (bc[i] == null) + return; + if (bc[i].prevx.isEmpty()) { + bc[i].prevx.addLast(Float.valueOf(bc[i].x)); + bc[i].prevy.addLast(Float.valueOf(bc[i].y)); + } else if (bc[i].prevx.size() == bc[i].capacity) { + bc[i].prevx.removeFirst(); + bc[i].prevy.removeFirst(); + } + + bc[i].x += bc[i].incX; + bc[i].y += bc[i].incY; + + float random = (float) Math.random(); + + // right + if (bc[i].x + bc[i].ball_size > width) { + bc[i].x = width - bc[i].ball_size; + bc[i].incX = random * -width / 16 - 1; + } + // left + if (bc[i].x < 0) { + bc[i].x = 0; + bc[i].incX = random * width / 16 + 1; + } + // bottom + if (bc[i].y + bc[i].ball_size > height) { + bc[i].y = (height - bc[i].ball_size) - 2; + bc[i].incY = random * -height / 16 - 1; + } + // top + if (bc[i].y < 0) { + bc[i].y = 0; + bc[i].incY = random * height / 16 + 1; + } + bc[i].prevx.addLast(Float.valueOf(bc[i].x)); + bc[i].prevy.addLast(Float.valueOf(bc[i].y)); + } + } + + public static void paint(GC gc, int width, int height) { + Device device = gc.getDevice(); + gc.setBackground(device.getSystemColor(SWT.COLOR_WHITE)); + gc.fillRectangle(0, 0, width, height); + + if (bc[0] == null) { + + for (int i = 0; i < bc.length; i++) { + bc[i] = new BallCollection((float) (Math.random() * width), (float) (Math.random() * height), + (float) (Math.random() * 10 - 5), (float) (Math.random() * 10 - 5), (int) (Math.random() * 30 + 20), + 50, new Color[] { device.getSystemColor(SWT.COLOR_GREEN) }); + } + +// bc[0] = new BallCollection((float) (Math.random() * width), (float) (Math.random() * height), +// (float) (Math.random() * 10 - 5), (float) (Math.random() * 10 - 5), (int) (Math.random() * 30 + 20), +// 50, new Color[] { device.getSystemColor(SWT.COLOR_GREEN) }); + + } + + for (BallCollection ballCollection : bc) { + for (int i = 0; i < ballCollection.prevx.size(); i++) { + Transform transform = new Transform(device); + transform.translate(ballCollection.prevx.get(ballCollection.prevx.size() - (i + 1)).floatValue(), + ballCollection.prevy.get(ballCollection.prevy.size() - (i + 1)).floatValue()); + gc.setTransform(transform); + transform.dispose(); + + Path path = new Path(device); + path.addArc(0, 0, ballCollection.ball_size, ballCollection.ball_size, 0, 360); + gc.setAlpha(255 - i * (255 / ballCollection.capacity)); + gc.setBackground(ballCollection.colors[0]); + gc.fillPath(path); + gc.drawPath(path); + path.dispose(); + } + } + } +} \ No newline at end of file diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetBarChart.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetBarChart.java new file mode 100644 index 00000000000..451b0c09b1f --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetBarChart.java @@ -0,0 +1,41 @@ +package org.eclipse.swt.examples.skia; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +/** + * Example usage of BarChart. + */ +public class SnippetBarChart { + private static final boolean USE_SKIA = true; + + public static void main(String[] args) { + Display display = new Display(); + Shell shell = new Shell(display); + shell.setText("BarChart Example"); + shell.setLayout(new GridLayout(1, false)); + + org.eclipse.swt.examples.accessibility.BarChart chart = new org.eclipse.swt.examples.accessibility.BarChart(shell, SWT.BORDER | (USE_SKIA ? SWT.SKIA : SWT.NONE)); + chart.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + chart.setTitle("Sample Bar Chart"); + chart.setColor(SWT.COLOR_GREEN); + chart.setValueMin(0); + chart.setValueMax(10); + chart.setValueIncrement(1); + chart.addData("A", 2); + chart.addData("B", 7); + chart.addData("C", 5); + chart.addData("D", 9); + chart.addData("E", 4); + + shell.setSize(400, 250); + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) display.sleep(); + } + display.dispose(); + } +} diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetCLabel.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetCLabel.java new file mode 100644 index 00000000000..659368a350c --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetCLabel.java @@ -0,0 +1,48 @@ +package org.eclipse.swt.examples.skia; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.CLabel; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.graphics.Image; + +/** + * Example usage of CLabel. + */ +public class SnippetCLabel { + public static final boolean USE_SKIA = true; + + public static void main(String[] args) { + Display display = new Display(); + Shell shell = new Shell(display); + shell.setText("CLabel Example"); + shell.setLayout(new GridLayout(1, false)); + + // Simple CLabel with text + CLabel label1 = new CLabel(shell, (USE_SKIA ? SWT.SKIA : 0) | SWT.NONE); + label1.setText("This is a CLabel with text."); + label1.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + // CLabel with image and text + Image image = display.getSystemImage(SWT.ICON_INFORMATION); + CLabel label2 = new CLabel(shell, (USE_SKIA ? SWT.SKIA : 0) | SWT.NONE); + label2.setText("CLabel with image and text."); + label2.setImage(image); + label2.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + // CLabel with alignment + CLabel label3 = new CLabel(shell, (USE_SKIA ? SWT.SKIA : 0) | SWT.NONE); + label3.setText("Right aligned CLabel."); + label3.setAlignment(SWT.RIGHT); + label3.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); + + shell.setSize(350, 150); + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) display.sleep(); + } + display.dispose(); + } +} \ No newline at end of file diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetCanvasAdvancedCompare.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetCanvasAdvancedCompare.java new file mode 100644 index 00000000000..afc1c1155c4 --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetCanvasAdvancedCompare.java @@ -0,0 +1,445 @@ +package org.eclipse.swt.examples.skia; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Path; +import org.eclipse.swt.graphics.Pattern; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.graphics.Region; +import org.eclipse.swt.graphics.Transform; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; + +public class SnippetCanvasAdvancedCompare { + + // working variable for the snippet + private static boolean skiaCurrentlyActive; + + public static void main(String[] args) { + final Display display = new Display(); + final Shell s = new Shell(display); + s.setLayout(new FillLayout()); + + final ScrolledComposite sc = new ScrolledComposite(s, SWT.V_SCROLL | SWT.H_SCROLL); + sc.setExpandHorizontal(true); + sc.setExpandVertical(true); + + final Composite shell = new Composite(sc, SWT.NONE); + sc.setContent(shell); + + shell.setLayout(new GridLayout(2, true)); + + text(shell, "Zeichne Text mit verschiedenen Farben"); + + coloredTextCanvas(shell); + + resetCanvasConfiguration(); + + activateSkiaRaster(); + coloredTextCanvas(shell); + resetCanvasConfiguration(); + + text(shell, "Zeichne Text AntiAlias-Modi"); + + coloredTextNoAACanvas(shell); + + activateSkiaRaster(); + coloredTextNoAACanvas(shell); + resetCanvasConfiguration(); + + text(shell, "Zeichne rechteckige Linien mit verschiedenen Strichstärken"); + + lineAttributesCanvas(shell); + activateSkiaRaster(); + lineAttributesCanvas(shell); + resetCanvasConfiguration(); + + text(shell, " Zeichne mit einem Path"); + + pathCanvas(shell); + + activateSkiaRaster(); + pathCanvas(shell); + resetCanvasConfiguration(); + + text(shell, " XOR-Mode"); + + xorMode(shell); + activateSkiaRaster(); + xorMode(shell); + resetCanvasConfiguration(); + + text(shell, " Geänderte Transformationsmatrix"); + + transformCanvas(shell); + activateSkiaRaster(); + transformCanvas(shell); + resetCanvasConfiguration(); + + text(shell, " Benutze Region"); + + regionCanvas(shell); + activateSkiaRaster(); + regionCanvas(shell); + resetCanvasConfiguration(); + + text(shell, " Zeichne mit einem Muster"); + + patternCanvas(shell); + + activateSkiaRaster(); + patternCanvas(shell); + resetCanvasConfiguration(); + + text(shell, " Anti-Aliasing für Grafik"); + + antiAliasCanvas(shell); + activateSkiaRaster(); + antiAliasCanvas(shell); + resetCanvasConfiguration(); + + text(shell, " Zeichne Bilder mit verschiedenen Alpha-Werten"); + + alphaCanvas(shell); + activateSkiaRaster(); + alphaCanvas(shell); + resetCanvasConfiguration(); + + text(shell, "Clipping Rectangle"); + + clipRectangleCanvas(shell); + activateSkiaRaster(); + clipRectangleCanvas(shell); + resetCanvasConfiguration(); + + text(shell, "Clipping Region"); + + clipCanvasRegion(shell); + activateSkiaRaster(); + clipCanvasRegion(shell); + resetCanvasConfiguration(); + + text(shell, "Clipping Path"); + + clipCanvasPath(shell); + activateSkiaRaster(); + clipCanvasPath(shell); + resetCanvasConfiguration(); + + final var size = shell.computeSize(SWT.DEFAULT, SWT.DEFAULT); + sc.setMinSize(size); + + s.open(); + while (!s.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + + display.dispose(); + } + + private static void resetCanvasConfiguration() { + skiaCurrentlyActive = false; + } + + private static void activateSkiaRaster() { + skiaCurrentlyActive = true; + } + + private static void coloredTextNoAACanvas(Composite shell) { + final Canvas coloredTextCanvas = createCanvas(shell); + coloredTextCanvas.setBackground(shell.getDisplay().getSystemColor(SWT.COLOR_YELLOW)); + coloredTextCanvas.addPaintListener(e -> { + final GC gc = e.gc; + + final boolean advanced = e.gc.getAdvanced(); + final int textAA = e.gc.getTextAntialias(); + + final var f = shell.getDisplay().getSystemFont(); + + final var fd = f.getFontData()[0]; + + fd.height = 20; + + final Font nf = new Font(shell.getDisplay(), new FontData[] { fd }); + + e.gc.setFont(nf); + + e.gc.setAdvanced(true); + e.gc.setTextAntialias(SWT.OFF); + + final Color blue = shell.getDisplay().getSystemColor(SWT.COLOR_BLUE); + gc.setForeground(blue); + gc.drawText("No AntiAlias Text", 10, 10); + + final Color red = shell.getDisplay().getSystemColor(SWT.COLOR_RED); + gc.setForeground(red); + e.gc.setTextAntialias(SWT.ON); + gc.drawText("AntiAlias Text", 10, 70); + + final Color black = shell.getDisplay().getSystemColor(SWT.COLOR_BLACK); + gc.setForeground(black); + e.gc.setTextAntialias(SWT.DEFAULT); + gc.drawText("Default Text", 10, 130); + + gc.setAdvanced(advanced); + gc.setTextAntialias(textAA); + + }); + + setGridData(coloredTextCanvas, 200); + + } + + private static void clipCanvasPath(Composite shell) { + final Canvas c = createCanvas(shell); + final var display = shell.getDisplay(); + + c.addPaintListener(e -> { + final var p = c.getSize(); + final int width = p.x; + final int height = p.y; + + final Path pa = new Path(display); + pa.addArc(0, 0, 100, 100, 90, 200); + + e.gc.setClipping(pa); + e.gc.setBackground(display.getSystemColor(SWT.COLOR_CYAN)); + + e.gc.fillRectangle(0, 0, width, height); + }); + + setGridData(c, 200); + + } + + private static void clipCanvasRegion(Composite shell) { + + final Canvas c = createCanvas(shell); + final var display = shell.getDisplay(); + + c.addPaintListener(e -> { + final var p = c.getSize(); + final int width = p.x; + final int height = p.y; + + final Region r = new Region(display); + r.add(new int[] { width / 2, 0, 0, height / 2, width / 2, height, width, height / 2 }); + e.gc.setClipping(r); + e.gc.setBackground(display.getSystemColor(SWT.COLOR_CYAN)); + + e.gc.fillPolygon(new int[] { 0, 0, width, 0, width / 2, height }); + }); + + setGridData(c, 200); + + } + + private static void clipRectangleCanvas(Composite shell) { + final Canvas c = createCanvas(shell); + final var display = shell.getDisplay(); + + c.addPaintListener(e -> { + final var p = c.getSize(); + final int width = p.x; + final int height = p.y; + e.gc.setClipping(20, 20, width - 40, height - 40); + e.gc.setBackground(display.getSystemColor(SWT.COLOR_CYAN)); + + e.gc.fillPolygon(new int[] { 0, 0, width, 0, width / 2, height }); + }); + + setGridData(c, 200); + + } + + private static void alphaCanvas(Composite shell) { + final Canvas alphaCanvas = createCanvas(shell); + final var display = shell.getDisplay(); + final Image image = new Image(display, 100, 50); + final GC gcImage = new GC(image); + gcImage.setBackground(display.getSystemColor(SWT.COLOR_GREEN)); + gcImage.fillRectangle(0, 0, 100, 50); + gcImage.dispose(); + + alphaCanvas.addPaintListener(e -> { + final GC gc = e.gc; + gc.setAlpha(128); + gc.drawImage(image, 10, 10); + }); + + setGridData(alphaCanvas); + + } + + private static void antiAliasCanvas(Composite shell) { + final Canvas antiAliasCanvas = createCanvas(shell); + antiAliasCanvas.addPaintListener(e -> { + final GC gc = e.gc; + gc.setAntialias(SWT.ON); + gc.drawOval(10, 10, 100, 50); + }); + + setGridData(antiAliasCanvas); + + } + + private static void patternCanvas(Composite shell) { + + final Canvas patternCanvas = createCanvas(shell); + patternCanvas.addPaintListener(e -> { + final GC gc = e.gc; + + final Pattern pattern = new Pattern(shell.getDisplay(), 10, 10, 100, 50, new Color(new RGB(255, 0, 0)), 100, + new Color(new RGB(0, 0, 255)), 255); + gc.setBackgroundPattern(pattern); + gc.fillRectangle(10, 10, 100, 50); + pattern.dispose(); + }); + + setGridData(patternCanvas); + + } + + private static void regionCanvas(Composite shell) { + final Canvas regionCanvas = createCanvas(shell); + regionCanvas.addPaintListener(e -> { + final GC gc = e.gc; + final Region region = new Region(); + region.add(new Rectangle(10, 10, 100, 50)); + region.add(new Rectangle(60, 30, 100, 50)); + gc.setClipping(region); + gc.setBackground(shell.getDisplay().getSystemColor(SWT.COLOR_GREEN)); + gc.fillRectangle(0, 0, 200, 100); + region.dispose(); + }); + + setGridData(regionCanvas); + + } + + private static void transformCanvas(Composite shell) { + final Canvas transformCanvas = createCanvas(shell); + transformCanvas.addPaintListener(e -> { + final GC gc = e.gc; + final Transform transform = new Transform(shell.getDisplay()); + transform.translate(50, 50); + transform.rotate(45); + gc.setTransform(transform); + gc.drawRectangle(-25, -25, 100, 50); + transform.dispose(); + }); + + setGridData(transformCanvas); + + } + + private static void xorMode(Composite shell) { + + final var dis = shell.getDisplay(); + + final Canvas xorModeCanvas = createCanvas(shell); + xorModeCanvas.setBackground(dis.getSystemColor(SWT.COLOR_BLACK)); + xorModeCanvas.addPaintListener(e -> { + final GC gc = e.gc; + gc.setXORMode(true); + gc.setForeground(shell.getDisplay().getSystemColor(SWT.COLOR_RED)); + gc.drawRectangle(10, 10, 100, 50); + gc.drawRectangle(30, 30, 60, 30); + gc.setXORMode(false); + }); + + setGridData(xorModeCanvas); + + } + + private static Canvas createCanvas(Composite shell) { + int style = skiaCurrentlyActive ? SWT.SKIA : SWT.NONE; + return new Canvas(shell, SWT.BORDER | style); + } + + private static void pathCanvas(Composite shell) { + final Canvas pathCanvas = createCanvas(shell); + pathCanvas.addPaintListener(e -> { + final GC gc = e.gc; + final Path path = new Path(shell.getDisplay()); + path.moveTo(10, 10); + path.lineTo(110, 60); + path.lineTo(60, 110); + path.close(); + gc.drawPath(path); + path.dispose(); + }); + + setGridData(pathCanvas); + + } + + private static void setGridData(Control c) { + setGridData(c, 150); + } + + private static void setGridData(Control c, int minHeight) { + final var g = new GridData(); + g.grabExcessHorizontalSpace = true; + g.grabExcessVerticalSpace = true; + g.horizontalAlignment = GridData.FILL; + g.verticalAlignment = GridData.FILL; + g.minimumHeight = minHeight; + c.setLayoutData(g); + } + + private static void lineAttributesCanvas(Composite shell) { + final Canvas lineAttributesCanvas = createCanvas(shell); + lineAttributesCanvas.addPaintListener(e -> { + final GC gc = e.gc; + gc.setLineWidth(5); + gc.setLineJoin(SWT.JOIN_ROUND); + gc.setLineCap(SWT.CAP_ROUND); + gc.drawRectangle(10, 10, 100, 50); + }); + + setGridData(lineAttributesCanvas); + + } + + private static void coloredTextCanvas(Composite shell) { + final Canvas coloredTextCanvas = createCanvas(shell); + coloredTextCanvas.setBackground(shell.getDisplay().getSystemColor(SWT.COLOR_YELLOW)); + coloredTextCanvas.addPaintListener(e -> { + final GC gc = e.gc; + final Color blue = shell.getDisplay().getSystemColor(SWT.COLOR_BLUE); + gc.setForeground(blue); + gc.drawText("Blauer Text", 10, 10); + }); + + setGridData(coloredTextCanvas); + + } + + private static void text(Composite shell, String string) { + final var l = new Label(shell, SWT.BORDER); + l.setText(string); + final var g = new GridData(); + g.horizontalSpan = 2; + g.grabExcessHorizontalSpace = true; + + g.horizontalAlignment = GridData.FILL; + l.setLayoutData(g); + + } +} \ No newline at end of file diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetCanvasCaret.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetCanvasCaret.java new file mode 100644 index 00000000000..27c52c4c124 --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetCanvasCaret.java @@ -0,0 +1,71 @@ +package org.eclipse.swt.examples.skia; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Caret; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +public class SnippetCanvasCaret { + public static void main(String[] args) { + Display display = new Display(); + Shell shell = new Shell(display); + shell.setText("Canvas Caret Demo"); + shell.setSize(400, 300); + shell.setLayout(new GridLayout(1, false)); // One column, vertical stack + + // Create two canvases stacked vertically + Canvas canvas1 = new Canvas(shell, SWT.BORDER | SWT.SKIA); + canvas1.setLayoutData(new GridData(GridData.FILL_BOTH)); + canvas1.setBackground(display.getSystemColor(SWT.COLOR_GREEN)); + Canvas canvas2 = new Canvas(shell, SWT.BORDER); + canvas2.setLayoutData(new GridData(GridData.FILL_BOTH)); + canvas2.setBackground(display.getSystemColor(SWT.COLOR_BLUE)); + + // Initialize carets for both canvases + Caret caret1 = new Caret(canvas1, SWT.NONE); + caret1.setSize(2, 30); + caret1.setVisible(true); + Caret caret2 = new Caret(canvas2, SWT.NONE); + caret2.setSize(2, 30); + caret2.setVisible(true); + + // Timer logic for moving the carets + final int steps = 20; // Number of positions + Runnable moveCarets = new Runnable() { + int pos = 0; + + @Override + public void run() { + if (canvas1.isDisposed() || caret1.isDisposed() || canvas2.isDisposed() || caret2.isDisposed()) + return; + + var ca = canvas2.getClientArea(); + var width = ca.width; + int x = (int) ((1f * pos) / (1f * steps) * width); + + if (pos < 10) { + caret1.setLocation(x, 15); + canvas1.setFocus(); + + } else { + caret2.setLocation(x, 15); + canvas2.setFocus(); + } + + pos = (pos + 1) % steps; + display.timerExec(1000, this); + } + }; + display.timerExec(0, moveCarets); + + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + display.dispose(); + } +} \ No newline at end of file diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetCanvasCompare.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetCanvasCompare.java new file mode 100644 index 00000000000..317870b5e19 --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetCanvasCompare.java @@ -0,0 +1,151 @@ +package org.eclipse.swt.examples.skia; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Path; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Shell; + +public class SnippetCanvasCompare { + + public static void main(String[] args) { + final Display display = new Display(); + final Shell shell = new Shell(display); + shell.setText("Snippet Canvas Compare"); + + + configureCanvas(shell, new Canvas(shell, SWT.DOUBLE_BUFFERED| SWT.H_SCROLL | SWT.BORDER), 1, "SWT Canvas"); + configureCanvas(shell, new Canvas(shell, SWT.DOUBLE_BUFFERED | SWT.H_SCROLL| SWT.SKIA | SWT.BORDER), 2, "SkiaGlCanvas"); + + shell.setSize(1500, 1000); + + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + display.dispose(); + } + + private static void configureCanvas(final Shell shell, final Canvas canvas, int index, String title) { + canvas.setSize(100, 100); +// canvas.setBackground(shell.getDisplay().getSystemColor(SWT.COLOR_YELLOW)); + shell.addListener(SWT.Resize, e -> onResize(canvas, index)); + canvas.addListener(SWT.Paint, e -> onPaint(e, title)); + } + + private static void onPaint(Event e, String title) { + final Display d = e.widget.getDisplay(); + final GC gc = e.gc; + + var bg = gc.getBackground(); + + gc.setBackground(d.getSystemColor(SWT.COLOR_YELLOW)); + gc.fillRectangle(new Rectangle(-100, -100, 3000, 3000)); + + gc.setBackground(bg); + + + gc.setForeground(d.getSystemColor(SWT.COLOR_RED)); + gc.drawRectangle(new Rectangle(0, 0, 100, 100)); + + gc.setForeground(d.getSystemColor(SWT.COLOR_BLUE)); + gc.drawRectangle(new Rectangle(100, 0, 100, 100)); + + gc.setForeground(d.getSystemColor(SWT.COLOR_GREEN)); + gc.drawRectangle(new Rectangle(0, 100, 100, 100)); + + final var img = d.getSystemImage(SWT.ICON_QUESTION); + gc.drawImage(img, 100, 100); + + gc.setForeground(d.getSystemColor(SWT.COLOR_RED)); + gc.drawLine(200, 100, 100, 200); + + gc.setForeground(d.getSystemColor(SWT.COLOR_CYAN)); + gc.drawFocus(200, 0, 100, 100); + + gc.setForeground(d.getSystemColor(SWT.COLOR_RED)); + gc.drawArc(200, 100, 100, 100, 90, 200); + + gc.setForeground(d.getSystemColor(SWT.COLOR_BLACK)); + gc.drawText(title, 100, 250); + + gc.setForeground(d.getSystemColor(SWT.COLOR_DARK_CYAN)); + gc.drawOval(0, 300, 100, 50); + + final Path p = new Path(d); + p.addRectangle(0, 400, 100, 100); + gc.setForeground(d.getSystemColor(SWT.COLOR_DARK_GREEN)); + gc.drawPath(p); + + gc.setForeground(d.getSystemColor(SWT.COLOR_GRAY)); + gc.drawRoundRectangle(0, 500, 100, 100, 50, 50); + + gc.setForeground(d.getSystemColor(SWT.COLOR_DARK_RED)); + gc.drawPoint(5, 600); + + final var bo = img.getBounds(); + gc.drawImage(img, 0, 0, bo.width /2 , bo.height /2 , 20 , 600, bo.width * 2, bo.height *2 ); + + gc.setForeground(d.getSystemColor(SWT.COLOR_DARK_MAGENTA)); + gc.drawText("Test\nTest2", 300, 0); + gc.drawText("Test2\nTest3", 300, 100, false); + gc.drawText("Transparent Text", 300,200, true); + + + gc.setForeground(d.getSystemColor(SWT.COLOR_DARK_GRAY)); + gc.drawPolygon(new int[] { + 400,2, // + 500,100, // + 400,100, // + 500,2 // + }); + + + gc.setForeground(d.getSystemColor(SWT.COLOR_DARK_YELLOW)); + gc.drawPolyline(new int[] { + 400,50, // + 500,100, // + 600,200 + + }); + + gc.setBackground(d.getSystemColor(SWT.COLOR_DARK_YELLOW)); + gc.fillArc(100, 300, 100, 100, 90, 200); + + gc.setBackground(d.getSystemColor(SWT.COLOR_DARK_GREEN)); + gc.fillGradientRectangle(100, 400, 100, 100, false); + + gc.fillOval(100, 500, 100, 50); + + + final Path p2 = new Path(d); + p2.addRectangle(100, 600, 100, 100); + gc.fillPath(p2); + + gc.fillPolygon(new int[] { + 100,700, // + 200,800, // + 100,800, // + 200,700 // + }); + + gc.fillRectangle(new Rectangle(100, 800, 100, 100)); + + gc.fillRoundRectangle(200, 800, 100, 100, 20, 20); + } + + private static void onResize(Canvas c, int index) { + final var ca = c.getShell().getClientArea(); + switch(index) { + case 1 -> c.setBounds(new Rectangle(0, 0, ca.width / 2, ca.height)); + case 2 -> c.setBounds(new Rectangle(ca.width / 2, 0, ca.width / 2, ca.height)); + default -> { /* nothing to do */ } + } + } + +} diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetCanvasText.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetCanvasText.java new file mode 100644 index 00000000000..173077ea460 --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetCanvasText.java @@ -0,0 +1,122 @@ +package org.eclipse.swt.examples.skia; + +import java.util.Random; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Shell; + +public class SnippetCanvasText { + final static boolean useSkia = true; + + + final static int style = SWT.DOUBLE_BUFFERED | (useSkia ? SWT.SKIA : SWT.NONE); + final static int LETTERS_PER_LINE = 2000; + final static int LINES = 60; + final static int SHIFT_LEFT = 2000; + + static String[] text = new String[LINES]; + + public static void main(String[] args) { + final Display display = new Display(); + final Shell shell = new Shell(display); + shell.setText("Snippet Canvas"); + // here you can switch between Canvas SkiaRasterCanvas and SkiaCanvas + final Canvas c = new Canvas(shell, style ); + + for (int j = 0; j < LINES; j++) { + text[j] = generateText(LETTERS_PER_LINE); + } + + c.setSize(100, 100); + + shell.addListener(SWT.Resize, e -> onResize(e, c)); + c.addListener(SWT.Paint, SnippetCanvasText::onPaint); + c.addListener(SWT.Paint, SnippetCanvasText::onPaint2); + + shell.setSize(1000, LETTERS_PER_LINE * 3 + 80); + + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + display.dispose(); + } + + static long startTime = System.currentTimeMillis(); + private static boolean printFrameRate = true; + private static int frames; + private static long lastFrame; + private static long framesToDraw; + + private static void onPaint(Event e) { + + final var s = ((Canvas) e.widget); + + final Point size = s.getSize(); + + long currentPosTime = System.currentTimeMillis() - startTime; + + currentPosTime = currentPosTime % 10000; + + final double position = (double) currentPosTime / (double) 10000; + + final int shift = (int) (position * size.x) - SHIFT_LEFT; + final int shiftDown = 20; + + + for(int j = 0 ; j < LINES; j++) { + e.gc.drawText(text[j], shift, shiftDown + 20 * j,true); + } + } + + private static void onPaint2(Event e) { + + final var s = ((Canvas) e.widget); + + if (printFrameRate) { + + if (System.currentTimeMillis() - lastFrame > 1000) { + framesToDraw = frames; + + frames = 0; + lastFrame = System.currentTimeMillis(); + } + frames++; + + e.gc.drawText("Frames: " + framesToDraw, 10, 10,true); + + } + + s.redraw(); + + } + + private static void onResize(Event e, Canvas c) { + + final var ca = c.getShell().getClientArea(); + + c.setSize(ca.width, ca.height); + + } + public static String generateText(int textLength) { + final int leftLimit = 97; // letter 'a' + final int rightLimit = 122; // letter 'z' + final Random random = new Random(); + + final String generatedString = random.ints(leftLimit, rightLimit + 1) + .limit(textLength) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + + + return generatedString; + } + + +} diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetCanvasTextOCX26.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetCanvasTextOCX26.java new file mode 100644 index 00000000000..3932f3ec2dc --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetCanvasTextOCX26.java @@ -0,0 +1,164 @@ +package org.eclipse.swt.examples.skia; + +import java.util.Random; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Shell; + +public class SnippetCanvasTextOCX26 { + final static boolean useSkia = true; + + final static int style = SWT.DOUBLE_BUFFERED | (useSkia ? SWT.SKIA : SWT.NONE); + final static int LETTERS_PER_LINE = 2000; + final static int LINES = 60; + final static int SHIFT_LEFT = 2000; + + final static int LEFT_START = 50; + static String[] text = new String[LINES]; + + public static void main(String[] args) { + final Display display = new Display(); + final Shell shell = new Shell(display); + shell.setText("Snippet Canvas"); + // here you can switch between Canvas SkiaRasterCanvas and SkiaCanvas + final Canvas c = new Canvas(shell, style ); + c.setBackground(display.getSystemColor(SWT.COLOR_BLACK)); + c.setForeground(display.getSystemColor(SWT.COLOR_WHITE)); + + for (int j = 0; j < LINES; j++) { + text[j] = generateText(LETTERS_PER_LINE); + } + + c.setSize(100, 100); + + shell.addListener(SWT.Resize, e -> onResize(e, c)); + c.addListener(SWT.Paint, SnippetCanvasTextOCX26::onPaint); + c.addListener(SWT.Paint, SnippetCanvasTextOCX26::onPaint2); + + shell.setSize(1000, LETTERS_PER_LINE * 3 + 80); + + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + display.dispose(); + } + + static long startTime = System.currentTimeMillis(); + private static boolean printFrameRate = true; + private static int frames; + private static long lastFrame; + private static long framesToDraw; + + private static void onPaint(Event e) { + + final var s = ((Canvas) e.widget); + + Font f = e.display.getSystemFont(); + + var fd = f.getFontData()[0]; + + fd.setHeight(200); + + e.gc.setAlpha(200); + var fontBefore = e.gc.getFont(); + var newFont = new Font(e.display, fd); + e.gc.setFont(newFont); + e.gc.setForeground(e.display.getSystemColor(SWT.COLOR_GREEN)); + e.gc.drawText("OCX 2026" ,LEFT_START,50,true ); + + var ocxSize = e.gc.textExtent("OCX 2026"); + + newFont.dispose(); + + fd.setHeight(50); + newFont = new Font(e.display, fd); + e.gc.setFont(newFont); + + if(useSkia) { + e.gc.drawText("GPU Accelerated Drawing With Skia SWT" ,LEFT_START,50 + ocxSize.y + 5,true ); + } else { + e.gc.drawText("CPU Drawing With Classical SWT" ,LEFT_START,50 + ocxSize.y + 5,true ); + } + newFont.dispose(); + + e.gc.setFont(fontBefore); + e.gc.setAlpha(255); + e.gc.setForeground(e.display.getSystemColor(SWT.COLOR_WHITE)); + + final var size = s.getSize(); + + long currentPosTime = System.currentTimeMillis() - startTime; + + currentPosTime = currentPosTime % 10000; + + final double position = (double) currentPosTime / (double) 10000; + + final int shift = (int) (position * size.x) - SHIFT_LEFT; + final int shiftDown = 20; + + + for(int j = 0 ; j < LINES; j++) { + e.gc.drawText(text[j], shift, shiftDown + 20 * j,true); + } + } + + private static void onPaint2(Event e) { + + final var s = ((Canvas) e.widget); + + if (printFrameRate) { + + if (System.currentTimeMillis() - lastFrame > 1000) { + framesToDraw = frames; + + frames = 0; + lastFrame = System.currentTimeMillis(); + } + frames++; + + var fontBefore = e.display.getSystemFont(); + var fd = fontBefore.getFontData()[0]; + fd.setHeight(50); + var newFont = new Font(e.display, fd); + e.gc.setFont(newFont); + + e.gc.drawText("Frames: " + framesToDraw, 10, 10,true); + + newFont.dispose(); + + } + + s.redraw(); + + } + + private static void onResize(Event e, Canvas c) { + + final var ca = c.getShell().getClientArea(); + + c.setSize(ca.width, ca.height); + + } + public static String generateText(int textLength) { + final int leftLimit = 97; // letter 'a' + final int rightLimit = 122; // letter 'z' + final Random random = new Random(); + + final String generatedString = random.ints(leftLimit, rightLimit + 1) + .limit(textLength) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append) + .toString(); + + + return generatedString; + } + + +} diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetClippingRectangle.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetClippingRectangle.java new file mode 100644 index 00000000000..e849e90b7d9 --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetClippingRectangle.java @@ -0,0 +1,155 @@ +package org.eclipse.swt.examples.skia; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Path; +import org.eclipse.swt.graphics.Region; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; + +public class SnippetClippingRectangle { + + + private static boolean skiaEnabled = true; + public static void main(String[] args) { + final Display display = new Display(); + final Shell s = new Shell(display); + s.setLayout(new FillLayout()); + + final Composite shell = new Composite(s, SWT.FILL | SWT.V_SCROLL); + + shell.setSize(1000, 2000); // Erhöhte Höhe für mehr Canvas + shell.setLayout(new GridLayout(2, true)); + + text(shell, "Clipping Rectangle"); + + resetCanvasConfiguration(); + clipRectangleCanvas(shell); + activateSkiaRaster(); + clipRectangleCanvas(shell); + resetCanvasConfiguration(); + + text(shell, "Clipping Region"); + + clipCanvasRegion(shell); + activateSkiaRaster(); + clipCanvasRegion(shell); + resetCanvasConfiguration(); + + text(shell, "Clipping Path"); + + clipCanvasPath(shell); + activateSkiaRaster(); + clipCanvasPath(shell); + resetCanvasConfiguration(); + + + s.open(); + while (!s.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + + display.dispose(); + } + + private static void clipCanvasPath(Composite shell) { + final Canvas c = createCanvas(shell); + final var display = shell.getDisplay(); + + c.addPaintListener(e -> { + final var p = c.getSize(); + final int width = p.x; + final int height = p.y; + + final Path r = new Path(display); + + r.addArc(width / 4, height / 4, width / 2, height / 2, 0, 360); + + e.gc.setClipping(r); + e.gc.setBackground(display.getSystemColor(SWT.COLOR_CYAN)); + + e.gc.fillRectangle(0, 0, width, height); + }); + + setGridData(c, 200); + } + + private static void resetCanvasConfiguration() { + skiaEnabled = false; + } + + private static void activateSkiaRaster() { + skiaEnabled = true; + } + + private static void clipCanvasRegion(Composite shell) { + final Canvas c = createCanvas(shell); + final var display = shell.getDisplay(); + + c.addPaintListener(e -> { + final var p = c.getSize(); + final int width = p.x; + final int height = p.y; + + final Region r = new Region(display); + r.add(new int[] { width / 2, 0, 0, height / 2, width / 2, height, width, height / 2 }); + e.gc.setClipping(r); + e.gc.setBackground(display.getSystemColor(SWT.COLOR_CYAN)); + + e.gc.fillPolygon(new int[] { 0, 0, width, 0, width / 2, height }); + }); + + setGridData(c, 200); + } + + private static void clipRectangleCanvas(Composite shell) { + final Canvas c = createCanvas(shell); + final var display = shell.getDisplay(); + + c.addPaintListener(e -> { + final var p = c.getSize(); + final int width = p.x; + final int height = p.y; + e.gc.setClipping(20, 20, width /2, height /2 ); + e.gc.setBackground(display.getSystemColor(SWT.COLOR_CYAN)); + + e.gc.fillPolygon(new int[] { 0, 0, width, 0, width / 2, height }); + }); + + setGridData(c, 200); + } + + private static Canvas createCanvas(Composite shell) { + int style = skiaEnabled ? SWT.SKIA : SWT.NONE; + return new Canvas(shell, SWT.BORDER | style); + } + + + private static void setGridData(Control c, int minHeight) { + final var g = new GridData(); + g.grabExcessHorizontalSpace = true; + g.grabExcessVerticalSpace = true; + g.horizontalAlignment = GridData.FILL; + g.verticalAlignment = GridData.FILL; + g.minimumHeight = minHeight; + c.setLayoutData(g); + } + + private static void text(Composite shell, String string) { + final var l = new Label(shell, SWT.BORDER); + l.setText(string); + final var g = new GridData(); + g.horizontalSpan = 2; + g.grabExcessHorizontalSpace = true; + g.horizontalAlignment = GridData.FILL; + l.setLayoutData(g); + } +} diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetCompareImages.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetCompareImages.java new file mode 100644 index 00000000000..bf62bdcdc0d --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetCompareImages.java @@ -0,0 +1,148 @@ +package org.eclipse.swt.examples.skia; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.ScrollBar; +import org.eclipse.swt.widgets.Shell; + +public class SnippetCompareImages { + + record ImageInfo(String name, Image image) { + } + + private static final String IMAGES_PATH = "images"; + private static final int IMAGE_SPACING = 10; + private final static ArrayList swtImages = new ArrayList<>(); + + public static void main(String[] args) { + Display display = new Display(); + Shell shell = new Shell(display); + shell.setText("Compare Images: GC vs Skija"); + shell.setLayout(new GridLayout(2, true)); + + Composite leftComposite = new Composite(shell, SWT.BORDER); + leftComposite.setLayout(new FillLayout()); + leftComposite.setLayoutData(new org.eclipse.swt.layout.GridData(SWT.FILL, SWT.FILL, true, true)); + Canvas gcCanvas = new Canvas(leftComposite, SWT.V_SCROLL | SWT.DOUBLE_BUFFERED); + gcCanvas.setLayoutData(new org.eclipse.swt.layout.GridData(SWT.FILL, SWT.FILL, true, true)); + ScrollBar gcScrollBar = gcCanvas.getVerticalBar(); + + Composite rightComposite = new Composite(shell, SWT.BORDER); + rightComposite.setLayout(new FillLayout()); + rightComposite.setLayoutData(new org.eclipse.swt.layout.GridData(SWT.FILL, SWT.FILL, true, true)); + Canvas skiaCanvas = new Canvas(rightComposite, SWT.V_SCROLL | SWT.SKIA); + skiaCanvas.setLayoutData(new org.eclipse.swt.layout.GridData(SWT.FILL, SWT.FILL, true, true)); + ScrollBar skiaScrollBar = skiaCanvas.getVerticalBar(); + // Load images + + swtImages.add(new ImageInfo("Question Icon", Display.getDefault().getSystemImage(SWT.ICON_QUESTION))); + swtImages.add(new ImageInfo("Error Icon", Display.getDefault().getSystemImage(SWT.ICON_ERROR))); + swtImages.add(new ImageInfo("Info Icon", Display.getDefault().getSystemImage(SWT.ICON_INFORMATION))); + + List imageFiles = new ArrayList<>(); + // Use working directory reliably + File imagesDir = new File(System.getProperty("user.dir"), IMAGES_PATH); + if (imagesDir.exists() && imagesDir.isDirectory()) { + for (File f : imagesDir.listFiles()) { + String name = f.getName().toLowerCase(); + if (name.endsWith(".png") || name.endsWith(".jpg") || name.endsWith(".jpeg") || name.endsWith(".gif") + || name.endsWith(".bmp") || name.endsWith(".ico")) { + + try { + Image img = new Image(display, f.getAbsolutePath()); + swtImages.add(new ImageInfo(f.getAbsolutePath(), img)); + imageFiles.add(f); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + + // Calculate total height + int totalHeight = IMAGE_SPACING; + for (ImageInfo img : swtImages) { + Rectangle bounds = img.image().getBounds(); + totalHeight += bounds.height + IMAGE_SPACING; + } + + // Scroll logic + gcScrollBar.setMaximum(totalHeight); + gcScrollBar.setThumb(600); // Initial thumb size, will resize with shell + skiaScrollBar.setMaximum(totalHeight); + skiaScrollBar.setThumb(600); // Initial thumb size, will resize with shell + + PaintListener gcPaintListener = new PaintListener() { + @Override + public void paintControl(PaintEvent e) { + int y = IMAGE_SPACING - gcScrollBar.getSelection(); + drawImages(y, e.gc); + } + }; + gcCanvas.addPaintListener(gcPaintListener); + + PaintListener swtPaintListener = new PaintListener() { + @Override + public void paintControl(PaintEvent e) { + int y = IMAGE_SPACING - skiaScrollBar.getSelection(); + drawImages(y, e.gc); + } + }; + + skiaCanvas.addPaintListener(swtPaintListener); + + gcScrollBar.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + gcCanvas.redraw(); + } + }); + + skiaScrollBar.addSelectionListener(new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + skiaCanvas.redraw(); + } + }); + + shell.setSize(1200, 700); // Initial size, but will resize + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + for (ImageInfo img : swtImages) + img.image().dispose(); + display.dispose(); + } + + private static void drawImages(int y, GC gc) { + gc.setInterpolation(SWT.HIGH); + for (ImageInfo img : swtImages) { + Rectangle bounds = img.image().getBounds(); + try { + gc.drawImage(img.image(), 10, y); + } catch (Exception ex) { + System.err.println(" Exception drawing image: " + ex); + } + gc.drawText(img.name(), 10 + bounds.width + 30, y); + y += bounds.height + IMAGE_SPACING; + } + } + +} \ No newline at end of file diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetCopyArea.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetCopyArea.java new file mode 100644 index 00000000000..4d978994539 --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetCopyArea.java @@ -0,0 +1,55 @@ +package org.eclipse.swt.examples.skia; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +public class SnippetCopyArea { + + final static boolean useSkia = true; + + public static void main(String[] args) { + Display display = new Display(); + Shell shell = new Shell(display); + shell.setText("Skija CopyArea paint=true Example"); + shell.setSize(400, 300); + + Canvas canvas = new Canvas(shell, useSkia ? SWT.SKIA : SWT.NONE); + canvas.setBounds(10, 10, 380, 280); + + // Create an image and draw something on it + Image image = new Image(display, 100, 100); + GC gcImage = new GC(image); + gcImage.setBackground(display.getSystemColor(SWT.COLOR_YELLOW)); + gcImage.fillRectangle(0, 0, 100, 100); + gcImage.setForeground(display.getSystemColor(SWT.COLOR_RED)); + gcImage.drawLine(0, 0, 100, 100); + gcImage.dispose(); + + canvas.addPaintListener(e -> { + + if(e.x == 20 && e.y == 20) { + return; + } + + // Draw the original image + e.gc.drawImage(image, 20, 20); + // Use copyArea to move a part of the image + // Rectangle: srcX, srcY, width, height, destX, destY + e.gc.copyArea(20, 20, 50, 50, 120, 20, true); // paint=true + // Draw a border around the copied area + e.gc.setForeground(display.getSystemColor(SWT.COLOR_BLUE)); + e.gc.drawRectangle(120, 20, 50, 50); + }); + + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) display.sleep(); + } + image.dispose(); + display.dispose(); + } +} \ No newline at end of file diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetDrawImageThenChange.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetDrawImageThenChange.java new file mode 100644 index 00000000000..056108ed9d7 --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetDrawImageThenChange.java @@ -0,0 +1,74 @@ +package org.eclipse.swt.examples.skia; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.layout.RowLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +public class SnippetDrawImageThenChange { + + private static final boolean USE_SKIA = true; + + public static void main(String[] args) { + + int canvasStyle = SWT.BORDER | (USE_SKIA ? SWT.SKIA : SWT.NONE); + + Display display = new Display(); + Shell shell = new Shell(display); + shell.setText("Image Cache Test"); + shell.setLayout(new RowLayout(SWT.HORIZONTAL)); + + int width = 120, height = 80; + // Erzeuge das Image + Image image = new Image(display, width, height); + GC gc = new GC(image); + gc.setBackground(display.getSystemColor(SWT.COLOR_YELLOW)); + gc.fillRectangle(0, 0, width, height); + gc.setForeground(display.getSystemColor(SWT.COLOR_BLUE)); + gc.drawText("Original", 10, 30); + gc.dispose(); + + Button b = new Button(shell, SWT.PUSH); + b.setText("Change Image"); + Canvas canvas1 = new Canvas(shell, canvasStyle); + canvas1.setSize(width, height); + canvas1.addPaintListener(e -> { + e.gc.drawImage(image, 0, 0); + }); + + Canvas canvas2 = new Canvas(shell, canvasStyle); + canvas2.setSize(width, height); + canvas2.addPaintListener(e -> { + e.gc.drawImage(image, 0, 0); + }); + + b.addListener(SWT.Selection, e -> { + + // Ändere das Image mit neuem GC + GC gc2 = new GC(image); + gc2.setForeground(display.getSystemColor(SWT.COLOR_RED)); + gc2.setLineWidth(4); + gc2.drawLine(0, 0, width, height); + gc2.drawText("Geändert", 10, 50); + gc2.dispose(); + canvas1.redraw(); + canvas2.redraw(); + + }); + + // Image-Größe + + shell.pack(); + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + image.dispose(); + display.dispose(); + } +} \ No newline at end of file diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetFillGradientRectangle.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetFillGradientRectangle.java new file mode 100644 index 00000000000..9adcfeeb558 --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetFillGradientRectangle.java @@ -0,0 +1,41 @@ +package org.eclipse.swt.examples.skia; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +public class SnippetFillGradientRectangle { + + final static boolean USE_SKIA = true; + + public static void main(String[] args) { + Display display = new Display(); + Shell shell = new Shell(display); + shell.setText("Skia FillGradientRectangle Example"); + shell.setSize(400, 300); + + Canvas canvas = new Canvas(shell, USE_SKIA ? SWT.SKIA : SWT.NONE); + canvas.setBounds(20, 20, 360, 240); + + canvas.addListener(SWT.Paint, event -> { + GC gc = event.gc; + Color color1 = display.getSystemColor(SWT.COLOR_RED); + Color color2 = display.getSystemColor(SWT.COLOR_YELLOW); + gc.setForeground(color1); + gc.setBackground(color2); + gc.fillGradientRectangle(40, 40, 200, 100, true); // vertical gradient + gc.setForeground(display.getSystemColor(SWT.COLOR_BLUE)); + gc.setBackground(display.getSystemColor(SWT.COLOR_GREEN)); + gc.fillGradientRectangle(40, 160, 200, 40, false); // horizontal gradient + }); + + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) display.sleep(); + } + display.dispose(); + } +} \ No newline at end of file diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetGetClippings.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetGetClippings.java new file mode 100644 index 00000000000..e493e6d0646 --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetGetClippings.java @@ -0,0 +1,100 @@ +package org.eclipse.swt.examples.skia; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.graphics.Region; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +public class SnippetGetClippings { + static boolean useSkia = true; + + public static void main(String[] args) { + Display display = new Display(); + Shell shell = new Shell(display); + shell.setText("GetClipping Example"); + shell.setSize(800, 400); + shell.setLayout(null); + + int style = SWT.DOUBLE_BUFFERED | SWT.BORDER | (useSkia ? SWT.SKIA : SWT.NONE); + + + // Rectangle clipping example + Canvas canvasRect = new Canvas(shell, style); + canvasRect.setBounds(20, 20, 350, 350); + canvasRect.addPaintListener(e -> { + GC gc = e.gc; + // Set rectangular clipping + Rectangle clipRect = new Rectangle(10, 10, 100, 100); + + gc.setBackground(display.getSystemColor(SWT.COLOR_YELLOW)); + gc.setClipping(clipRect); + gc.fillRectangle(0, 0, 120, 120); + + int shift = 130; + + // Draw something + // Get clipping rectangle + Rectangle receivedClip = gc.getClipping(); + receivedClip.y += shift; + + gc.setBackground(display.getSystemColor(SWT.COLOR_RED)); + gc.setClipping(receivedClip); + gc.fillRectangle(receivedClip); + gc.drawText("getClipping()", 20, 60 +shift); + + Region r = new Region(); + gc.getClipping(r); + + r.translate(0, shift); + + gc.setBackground(display.getSystemColor(SWT.COLOR_GREEN)); + gc.setClipping(r); + gc.fillRectangle(new Rectangle(0, 0, 1200, 1020)); + gc.drawText("getClipping(Region)", 20, 60+ 2*shift); + + + }); + + // Region (circle) clipping example + Canvas canvasRegion = new Canvas(shell, style); + canvasRegion.setBounds(400, 20, 350, 350); + canvasRegion.addPaintListener(e -> { + GC gc = e.gc; + // Set rectangular clipping + Rectangle clipRect = new Rectangle(10, 10, 100, 100); + Region region = new Region(); + region.add(clipRect); + + gc.setBackground(display.getSystemColor(SWT.COLOR_YELLOW)); + gc.setClipping(clipRect); + gc.fillRectangle(0, 0, 120, 120); + + int shift = 130; + + // Draw something + // Get clipping rectangle + Region ret = new Region(); + gc.getClipping(ret); + + gc.setBackground(display.getSystemColor(SWT.COLOR_RED)); + + ret.translate(0, 130); + + gc.setClipping(ret); + + gc.setBackground(display.getSystemColor(SWT.COLOR_GREEN)); + gc.setClipping(ret); + gc.fillRectangle(new Rectangle(0, 0, 1200, 1020)); + gc.drawText("Region Clipping", 20, 60+shift); + }); + + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) display.sleep(); + } + display.dispose(); + } +} \ No newline at end of file diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetGifInterpolationLoader.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetGifInterpolationLoader.java new file mode 100644 index 00000000000..514e75a389a --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetGifInterpolationLoader.java @@ -0,0 +1,139 @@ +package org.eclipse.swt.examples.skia; + +import java.io.IOException; +import java.io.InputStream; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.graphics.Device; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.graphics.Transform; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +public class SnippetGifInterpolationLoader { + + final static boolean useSkia = true; + + static Image loadImage(Device device, Class clazz, String string) { + try (InputStream stream = clazz.getResourceAsStream(string)) { + if (stream == null) + return null; + return new Image(device, stream); + } catch (SWTException ex) { + } catch (IOException ex) { + } + return null; + } + + public static void main(String[] args) { + + Display display = new Display(); + final var image = SnippetGifInterpolationLoader.loadImage(display, SnippetGifInterpolationLoader.class, "home_nav.gif"); + + Shell s = new Shell(display); + s.setLayout(new FillLayout()); + + int style = useSkia ? (SWT.FILL | SWT.SKIA ) : SWT.FILL; + + Canvas c = new Canvas(s, style ); + c.setBackground(display.getSystemColor(SWT.COLOR_RED)); + c.addPaintListener(new PaintListener() { + + @Override + public void paintControl(PaintEvent e) { + e.gc.drawImage(image, 0, 0); + var gc = e.gc; + var width = c.getBounds().width; + var height = c.getBounds().height; + Rectangle bounds = image.getBounds(); + + String text; + Point size = gc.stringExtent("OriginalText"); + + + float scaleX = 10f; + float scaleY = 10f; + var device = e.gc.getDevice(); + + Transform transform = new Transform(device); + transform.translate((width - (bounds.width * scaleX + 10) * 4) / 2, 25 + bounds.height + size.y + + (height - (25 + bounds.height + size.y + bounds.height*scaleY)) / 2); + transform.scale(scaleX, scaleY); + + // --- draw strings --- + float[] point = new float[2]; + text = "None"; //$NON-NLS-1$ + size = gc.stringExtent(text); + point[0] = (scaleX*bounds.width + 5 - size.x)/(2*scaleX); + point[1] = bounds.height; + transform.transform(point); + gc.drawString(text, (int)point[0], (int)point[1], true); + + text = "Low"; //$NON-NLS-1$ + size = gc.stringExtent(text); + point[0] = (scaleX*bounds.width + 5 - size.x)/(2*scaleX) + bounds.width; + point[1] = bounds.height; + transform.transform(point); + gc.drawString(text, (int)point[0], (int)point[1], true); + + text = "Default"; //$NON-NLS-1$ + size = gc.stringExtent(text); + point[0] = (scaleX*bounds.width + 5 - size.x)/(2*scaleX) + 2*bounds.width; + point[1] = bounds.height; + transform.transform(point); + gc.drawString(text, (int)point[0], (int)point[1], true); + + text = "High"; //$NON-NLS-1$ + size = gc.stringExtent(text); + point[0] = (scaleX*bounds.width + 5 - size.x)/(2*scaleX) + 3*bounds.width; + point[1] = bounds.height; + transform.transform(point); + gc.drawString(text, (int)point[0], (int)point[1], true); + + gc.setTransform(transform); + transform.dispose(); + + // --- draw images --- + + // no interpolation + gc.setInterpolation(SWT.NONE); + gc.drawImage(image, 0, 0); + + // low interpolation + gc.setInterpolation(SWT.LOW); + gc.drawImage(image, bounds.width, 0); + + // default interpolation + gc.setInterpolation(SWT.DEFAULT); + gc.drawImage(image, 2*bounds.width, 0); + + // high interpolation + gc.setInterpolation(SWT.HIGH); + gc.drawImage(image, 3*bounds.width, 0); + + } + }); + + + s.open(); + s.setSize(500, 500); + + while(!s.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + + display.dispose(); + + + + } + +} diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetImageToPNG.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetImageToPNG.java new file mode 100644 index 00000000000..657e7a3a52b --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetImageToPNG.java @@ -0,0 +1,118 @@ +package org.eclipse.swt.examples.skia; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.graphics.ImageLoader; +import org.eclipse.swt.widgets.Display; + +public class SnippetImageToPNG { + + final static boolean DELETE_OUTPUT_DIR = true; + + record ImageInfo(String name, Image image) { + } + + record FormatInfo(String format, int type) { + } + + + private final static List formats = List.of( +// new FormatInfo(".gif", SWT.IMAGE_GIF), // SWT does not support saving GIFs for these images +// new FormatInfo(".ico", SWT.IMAGE_ICO), // SWT does not support saving ICOs for these images + new FormatInfo(".bmp", SWT.IMAGE_BMP), + new FormatInfo(".jpef", SWT.IMAGE_JPEG), + new FormatInfo(".png", SWT.IMAGE_PNG) + ); + + + private static final String IMAGES_PATH = "images"; + private final static ArrayList swtImages = new ArrayList<>(); + + public static void main(String[] args) { + Display display = new Display(); + + // Load images + swtImages.add(new ImageInfo("question", Display.getDefault().getSystemImage(SWT.ICON_QUESTION))); + swtImages.add(new ImageInfo("error", Display.getDefault().getSystemImage(SWT.ICON_ERROR))); + swtImages.add(new ImageInfo("info", Display.getDefault().getSystemImage(SWT.ICON_INFORMATION))); + + List imageFiles = new ArrayList<>(); + // Use working directory reliably + File imagesDir = new File(System.getProperty("user.dir"), IMAGES_PATH); + if (imagesDir.exists() && imagesDir.isDirectory()) { + for (File f : imagesDir.listFiles()) { + String name = f.getName().toLowerCase(); + if (name.endsWith(".png") || name.endsWith(".jpg") || name.endsWith(".jpeg") || name.endsWith(".gif") + || name.endsWith(".bmp") || name.endsWith(".ico")) { + try { + swtImages.add(new ImageInfo(name, new Image(display, f.getAbsolutePath()))); + imageFiles.add(f); + } catch (Exception e) { + // skip invalid image + } + } + } + } + + File f = new File("./out"); + + if (f.exists()) { + for (File oldFile : f.listFiles()) { + oldFile.delete(); + } + } + f.mkdirs(); + + for (FormatInfo format : formats) { + System.out.println(format.format + " (" + format.type + "):"); + int i = 0; + for (ImageInfo img : swtImages) { + +// printHasAlpha(img, "original image " + i); + + ImageLoader loader = new ImageLoader(); + loader.data = new ImageData[] { img.image.getImageData() }; + String outPath = "./out/out" + i + format.format; + loader.save(outPath, format.type); + + if (format.type == SWT.IMAGE_PNG) { + printHasAlpha(outPath); + } + + i++; + } + + } + + if (DELETE_OUTPUT_DIR) { + for (File oldFile : f.listFiles()) { + oldFile.delete(); + } + f.delete(); + + } + } + +// private static void printHasAlpha(ImageInfo img, String outPath) { +// // Transparenzprüfung +// ImageData pngData = img.image.getImageData(); +// boolean hasAlpha = pngData.alphaData != null || pngData.alpha != -1; +// System.out.println("Path: " + img.name + " hasAlpha=" + hasAlpha + " alphaData=" +// + (pngData.alphaData != null ? pngData.alphaData.length : "null") + " alpha=" + pngData.alpha); +// +// } + + private static void printHasAlpha(String outPath) { + // Transparenzprüfung + ImageData pngData = new ImageLoader().load(outPath)[0]; + boolean hasAlpha = pngData.alphaData != null || pngData.alpha != -1; + System.out.println("Path: " + outPath + " hasAlpha=" + hasAlpha + " alphaData=" + + (pngData.alphaData != null ? pngData.alphaData.length : "null") + " alpha=" + pngData.alpha); + } + +} diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetPattern.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetPattern.java new file mode 100644 index 00000000000..441a4c106ab --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetPattern.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * Copyright (c) 2000, 2016 IBM Corporation and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * IBM Corporation - initial API and implementation + * SAP SE - skia support + *******************************************************************************/ +package org.eclipse.swt.examples.skia; + +/* + * Fill a shape with a predefined pattern + * + * For a list of all SWT example snippets see + * http://www.eclipse.org/swt/snippets/ + * + * @since 3.1 + */ +import org.eclipse.swt.SWT; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.Pattern; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +public class SnippetPattern { + + final static boolean USE_SKIA = true; + + static int style = USE_SKIA ? SWT.SKIA : SWT.NONE; + final static int WIDTH = 100; + final static int HEIGHT = 100; + +public static void main(String[] args) { + + Display display = new Display(); + //define a pattern on an image + final Image image = new Image(display, WIDTH, HEIGHT); + Color blue = display.getSystemColor(SWT.COLOR_BLUE); + Color yellow = display.getSystemColor(SWT.COLOR_YELLOW); + Color white = display.getSystemColor(SWT.COLOR_WHITE); + GC gc = new GC(image); + gc.setBackground(white); + gc.setForeground(yellow); + gc.fillGradientRectangle(0, 0, 1000, 1000, true); + for (int i=-500; i<1000; i+=10) { + gc.setForeground(blue); + gc.drawLine(i, 0, 500 + i, 1000); + gc.drawLine(500 + i, 0, i, 1000); + } + gc.drawRectangle(5, 5, WIDTH - 11, HEIGHT -11); + gc.dispose(); + final Pattern pattern; + try { + pattern = new Pattern(display, image); +// pattern = new Pattern(display, 0, 0, 100, 100, blue, yellow); + } catch (SWTException e) { + //Advanced Graphics not supported. + //This new API requires the Cairo Vector engine on GTK and GDI+ on Windows. + System.out.println(e.getMessage()); + display.dispose(); + return; + } + + Shell shell = new Shell(display); + shell.setText("SnippetPattern"); + shell.setLayout(new FillLayout()); + Canvas c = new Canvas(shell, SWT.DOUBLE_BUFFERED | style); + c.addListener(SWT.Paint, event -> { + Rectangle r = ((Composite)event.widget).getClientArea(); + GC gc1 = event.gc; + gc1.setBackgroundPattern(pattern); + gc1.fillRectangle(5, 5, r.width - 10, r.height - 10); + }); + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + image.dispose(); + pattern.dispose(); + display.dispose(); +} +} \ No newline at end of file diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetPattern2.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetPattern2.java new file mode 100644 index 00000000000..ccecad6bbff --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetPattern2.java @@ -0,0 +1,89 @@ +package org.eclipse.swt.examples.skia; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Pattern; +import org.eclipse.swt.graphics.RGBA; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +public class SnippetPattern2 { + + + public static void main(String[] args) { + final Display display = new Display(); + final Shell s = new Shell(display); + s.setLayout(new FillLayout()); + + final Composite shell = new Composite(s, SWT.FILL | SWT.V_SCROLL); + final GridLayout gridLayout = new GridLayout(2, false); + shell.setLayout(gridLayout); + + patternCanvas(shell); + + activateSkiaRaster(); + patternCanvas(shell); + resetCanvasConfiguration(); + + s.open(); + while (!s.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + + display.dispose(); + } + + private static boolean skiaEnabled = false; + private static void resetCanvasConfiguration() { + skiaEnabled = false; + } + + private static void activateSkiaRaster() { + skiaEnabled = true; + } + + private static void patternCanvas(Composite shell) { + + final Canvas patternCanvas = createCanvas(shell); + patternCanvas.addPaintListener(e -> { + final GC gc = e.gc; +// in the RGBA color for the pattern the alpha value will be ignored, the alpha values passed to the Pattern constructor will be used + final Pattern pattern = new Pattern(shell.getDisplay(), 10, 10, 100, 50, + new Color(new RGBA(255, 0, 0, 50)), 100, new Color(new RGBA(0, 0, 255, 100)), 200); + gc.setBackgroundPattern(pattern); + gc.fillRectangle(10, 10, 100, 50); + pattern.dispose(); + }); + + setGridData(patternCanvas); + + } + + private static Canvas createCanvas(Composite shell) { + int style = skiaEnabled ? SWT.SKIA : SWT.NONE; + return new Canvas(shell, SWT.BORDER | style); + } + + private static void setGridData(Control c) { + setGridData(c, 150); + } + + private static void setGridData(Control c, int minHeight) { + final var g = new GridData(); + g.grabExcessHorizontalSpace = true; + g.grabExcessVerticalSpace = true; + g.horizontalAlignment = GridData.FILL; + g.verticalAlignment = GridData.FILL; + g.minimumHeight = minHeight; + c.setLayoutData(g); + } +} diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetRasterSkiaCanvas.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetRasterSkiaCanvas.java new file mode 100644 index 00000000000..5182e79af9e --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetRasterSkiaCanvas.java @@ -0,0 +1,144 @@ +package org.eclipse.swt.examples.skia; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Shell; + +public class SnippetRasterSkiaCanvas { + + final static boolean useSkia = true; + + final static int RECTANGLES_PER_LINE = 200; + + static class RecDraw{ + + public RecDraw(int xPos, int yPos, Color c) { + super(); + this.xPos = xPos; + this.yPos = yPos; + this.c = c; + } + int xPos ,yPos; + Color c; + + } + + static RecDraw[][] recDraws = new RecDraw[RECTANGLES_PER_LINE][RECTANGLES_PER_LINE]; + private static int style = useSkia ? SWT.SKIA : SWT.NONE; + int width = 2; + + public static void main(String[] args) { + + final Display display = new Display(); + final Shell shell = new Shell(display); + shell.setText("Snippet Canvas"); + final Canvas c = new Canvas(shell, SWT.DOUBLE_BUFFERED | style); + + for( int x = 0 ; x < RECTANGLES_PER_LINE ; x++ ) { + for(int y = 0 ; y < RECTANGLES_PER_LINE ; y++) { + + recDraws[x][y] = new RecDraw( x*2,y*2,Display.getDefault().getSystemColor((x+y )% 16 )); + + } + } + + c.setSize(100, 100); + + shell.addListener(SWT.Resize, e -> onResize(e, c)); + c.addListener(SWT.Paint, SnippetRasterSkiaCanvas::onPaint); + c.addListener(SWT.Paint, SnippetRasterSkiaCanvas::onPaint2); + + shell.setSize(1000, RECTANGLES_PER_LINE*3+80); + + + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) { + c.redraw(); + } + } + display.dispose(); + } + + + static long startTime = System.currentTimeMillis(); + private static boolean printFrameRate = true; + private static int frames; + private static long lastFrame; + private static long framesToDraw; + + private static void onPaint(Event e) { + + + final var s = ((Canvas) e.widget); + + // surface.getCanvas().clear(0xFFFFFFFF); + + final Point size = s.getSize(); + + long currentPosTime = System.currentTimeMillis() - startTime; + + currentPosTime = currentPosTime % 10000; + + final double position = (double) currentPosTime / (double) 10000; + + final int shift = (int) (position * size.x); + final int shiftDown = 20; + + for( int x = 0 ; x < RECTANGLES_PER_LINE ; x++ ) { + for(int y = 0 ; y < RECTANGLES_PER_LINE ; y++) { + + final var rec = recDraws[x][y]; + e.gc.setForeground(rec.c); + e.gc.drawRectangle(shift + rec.xPos ,shiftDown + rec.yPos, 2,2 ); + + } + } + + + // int colorAsRGB = 0xFF42FFF4; + // int colorRed = 0xFFFF0000; + // int colorGreen = 0xFF00FF00; + // int colorBlue = 0xFF0000FF; + // + // e.gc.setForeground(s.getDisplay().getSystemColor(SWT.COLOR_RED)); + + + } + + private static void onPaint2(Event e) { + + if (printFrameRate) { + + if (System.currentTimeMillis() - lastFrame > 1000) { + // System.out.println("Frames: " + frames); + framesToDraw = frames; + + + frames = 0; + lastFrame = System.currentTimeMillis(); + } + frames++; + + e.gc.drawText("Frames: " + framesToDraw, 10,10); + + } + + + } + + + + private static void onResize(Event e, Canvas c) { + + final var ca = c.getShell().getClientArea(); + + c.setSize(ca.width, ca.height); + + } + +} diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetRegion.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetRegion.java new file mode 100644 index 00000000000..2d317648b0c --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetRegion.java @@ -0,0 +1,106 @@ +package org.eclipse.swt.examples.skia; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.graphics.Region; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Shell; + +public class SnippetRegion { + + final static Region r = new Region(); + + public static void main(String[] args) { + + for (int re = 0; re < 30; re++) { + r.add(new Rectangle(1 + 3 * re, 2 + 3 * re, 40, 40)); + } + r.subtract(15, 15, 20, 20); + + final Region inters = new Region(); + inters.add(new Rectangle(20, 20, 60, 60)); + r.intersect(inters); + + + r.translate(15, 15); + + r.add(toArray(generateCirclePoints(10, 30, 0))); + + final Display display = Display.getDefault(); + final Shell shell = new Shell(display); + shell.setText("Snippet Canvas"); + // here you can switch between Canvas SkiaRasterCanvas and SkiaCanvas + + setupCanvas(shell, SWT.DOUBLE_BUFFERED | SWT.SKIA, new Point(0, 0)); + setupCanvas(shell, SWT.DOUBLE_BUFFERED, new Point(100, 100)); + + shell.setSize(1000, 1000); + + shell.open(); + while (!shell.isDisposed()) { + display.readAndDispatch(); + } + display.dispose(); + + } + + public static int[] toArray(int[][] points) { + final int l = points.length; + final int[] ret = new int[l *2]; + for( int i = 0 ; i < l ; i++) { + ret[2*i] = points[i][0]; + ret[2*i+1] = points[i][1]; + } + + return ret; + + } + + + public static int[][] generateCirclePoints(int numPoints, int radius, int translate) { + final int[][] points = new int[numPoints][2]; + final double angleIncrement = 2 * Math.PI / numPoints; + + for (int i = 0; i < numPoints; i++) { + final double angle = i * angleIncrement; + final int x = (int) (radius * Math.cos(angle)) + translate; + final int y = (int) (radius * Math.sin(angle)) + translate; + points[i][0] = x; + points[i][1] = y; + } + + return points; + } + + private static void setupCanvas(Shell shell, int style, Point loc) { + + final Canvas c1 = new Canvas(shell, style); + c1.setSize(100, 100); + c1.setBackground(shell.getDisplay().getSystemColor(SWT.COLOR_CYAN)); + c1.addListener(SWT.Paint, SnippetRegion::onPaint); + c1.setLocation(loc); + + } + + private static void onPaint(Event e) { + + // surface.getCanvas().clear(0xFFFFFFFF); + + e.gc.setClipping(r); + + e.gc.setBackground(e.display.getSystemColor(SWT.COLOR_RED)); + e.gc.fillRectangle(0, 0, 100, 100); + + // int colorAsRGB = 0xFF42FFF4; + // int colorRed = 0xFFFF0000; + // int colorGreen = 0xFF00FF00; + // int colorBlue = 0xFF0000FF; + // + // e.gc.setForeground(s.getDisplay().getSystemColor(SWT.COLOR_RED)); + + } + +} diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetSkiaDirectDrawing.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetSkiaDirectDrawing.java new file mode 100644 index 00000000000..8f47792f27b --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetSkiaDirectDrawing.java @@ -0,0 +1,50 @@ +package org.eclipse.swt.examples.skia; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ControlEvent; +import org.eclipse.swt.events.ControlListener; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +public class SnippetSkiaDirectDrawing { + + public static void main(String[] arg) { + + + final Display d = new Display(); + final Shell s = new Shell(d); + s.setLayout(new FillLayout()); + + final Canvas c = new Canvas(s, SWT.DOUBLE_BUFFERED | SWT.FILL | SWT.SKIA ); + + c.setBackground(d.getSystemColor(SWT.COLOR_RED)); + + s.addControlListener(new ControlListener() { + + @Override + public void controlResized(ControlEvent e) { + c.setSize(s.getSize()); + + } + + @Override + public void controlMoved(ControlEvent e) { + + } + }); + + s.open(); + + while (!s.isDisposed()) { + d.readAndDispatch(); + if(!s.isDisposed()) + c.redraw(); + } + + d.close(); + + } + +} \ No newline at end of file diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetSkiaFonts.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetSkiaFonts.java new file mode 100644 index 00000000000..99c23ac1a25 --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetSkiaFonts.java @@ -0,0 +1,112 @@ +package org.eclipse.swt.examples.skia; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +public class SnippetSkiaFonts { + + final static int stepSize = 60; + + public static void main(String[] args) { + final Display display = new Display(); + final Shell shell = new Shell(display, SWT.V_SCROLL | SWT.SHELL_TRIM); + shell.setText("Available Fonts"); + shell.setSize(800, 600); + shell.setLayout(new GridLayout(2,true)); + + final Canvas canvas = new Canvas(shell, SWT.BORDER); + final Canvas skiaCanvas = new Canvas(shell, SWT.BORDER | SWT.SKIA); + + setGridData(canvas, 300); + setGridData(skiaCanvas, 300); + + final var pl = (PaintListener) event -> { + final GC gc = event.gc; + final FontData[] fontDataArrayScalable = display.getFontList(null, true); + final FontData[] fontDataArrayNotScalable = display.getFontList(null, false); + int y = 10 - shell.getVerticalBar().getSelection() ; + + // Courier is a bitmap font on windows, skia does not support it... + final Font font1 = new Font(display, "Courier", 20, SWT.NONE); + y = drawFont(gc,font1,"Courier",y); + + + for (final FontData fd : fontDataArrayScalable) { + + fd.setHeight(20); + if( y < -stepSize || y > canvas.getSize().y ) { + y += stepSize; + continue; + } + + final Font font = new Font(display, fd); + y = drawFont(gc,font,"'" + fd.getName() +"'",y); + + } + + for (final FontData fd : fontDataArrayNotScalable) { + + if( y < -stepSize || y > canvas.getSize().y ) { + y += stepSize; + continue; + } + + fd.setHeight(20); + final Font font = new Font(display, fd); + y = drawFont(gc,font,"'" + fd.getName() +"'",y); + } + + shell.getVerticalBar().setMaximum(y + shell.getVerticalBar().getSelection()); + }; + + canvas.addPaintListener(pl ); + skiaCanvas.addPaintListener(pl); + + + shell.getVerticalBar().addListener(SWT.Selection, e -> { + + canvas.redraw(); + skiaCanvas.redraw(); + + }); + + + + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + display.dispose(); + } + + private static int drawFont(GC gc, Font font1, String fontName1, int y) { + + gc.setFont(font1); + gc.drawText(fontName1, 20, y); + font1.dispose(); // Frees system resources + y += stepSize; + + return y; + } + + private static void setGridData(Control c, int minHeight) { + final var g = new GridData(); + g.grabExcessHorizontalSpace = true; + g.grabExcessVerticalSpace = true; + g.horizontalAlignment = GridData.FILL; + g.verticalAlignment = GridData.FILL; + g.minimumHeight = minHeight; + c.setLayoutData(g); + } +} \ No newline at end of file diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetSkijaCanvas.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetSkijaCanvas.java new file mode 100644 index 00000000000..315feb622e7 --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetSkijaCanvas.java @@ -0,0 +1,124 @@ +package org.eclipse.swt.examples.skia; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Shell; + +public class SnippetSkijaCanvas { + final static boolean useSkia = true; + + static final int RECTANGLES_PER_LINE = 200; + + + static record RecDraw(int xPos, int yPos, Color c) { + } + + static int style = SWT.FILL | SWT.DOUBLE_BUFFERED | (useSkia ? SWT.SKIA : SWT.NONE); + private static final RecDraw[][] recDraws = new RecDraw[RECTANGLES_PER_LINE][RECTANGLES_PER_LINE]; + private static long minFrameRate = Long.MAX_VALUE; + private static long maxFrameRate = 0; + private static long sum = 0; + private static double count = 0.0; + private static double average = 0; + + public static void main(String[] args) { + final Display display = new Display(); + final Shell shell = new Shell(display); + shell.setText("Snippet 1"); + + shell.setLayout(new FillLayout()); + final Canvas c = new Canvas(shell,style); + + + for (int x = 0; x < RECTANGLES_PER_LINE; x++) { + for (int y = 0; y < RECTANGLES_PER_LINE; y++) { + recDraws[x][y] = new RecDraw(x * 2, y * 2, Display.getDefault().getSystemColor((x + y) % 16)); + } + } + + c.setSize(100, 100); + + shell.addListener(SWT.Resize, e -> onResize(e, c)); + c.addListener(SWT.Paint, SnippetSkijaCanvas::onPaint); + c.addListener(SWT.Paint, SnippetSkijaCanvas::onPaint2); + + shell.setSize(1000, RECTANGLES_PER_LINE * 3 + 80); + + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + display.dispose(); + } + + static long startTime = System.currentTimeMillis(); + private static boolean printFrameRate = true; + private static int frames; + private static long lastFrame; + private static long framesToDraw; + + private static void onPaint(Event e) { + final var s = ((Canvas) e.widget); + + final Point size = s.getSize(); + long currentPosTime = System.currentTimeMillis() - startTime; + currentPosTime = currentPosTime % 10000; + + final double position = (double) currentPosTime / (double) 10000; + final int shift = (int) (position * size.x); + final int shiftDown = 20; + + for (int x = 0; x < RECTANGLES_PER_LINE; x++) { + for (int y = 0; y < RECTANGLES_PER_LINE; y++) { + final var rec = recDraws[x][y]; + e.gc.setForeground(rec.c); + e.gc.drawRectangle(shift + rec.xPos, shiftDown + rec.yPos, 2, 2); + + } + } + } + + private static void onPaint2(Event e) { + final var s = ((Canvas) e.widget); + + if (printFrameRate) { + if (System.currentTimeMillis() - lastFrame > 1000) { + framesToDraw = frames; + frames = 0; + lastFrame = System.currentTimeMillis(); + if(framesToDraw != 0) { + minFrameRate = Math.min(minFrameRate, framesToDraw); + } + maxFrameRate = Math.max(maxFrameRate, framesToDraw); + sum += framesToDraw; + count++; + average = sum/count; + } + frames++; + e.gc.drawText("Frames: min: " + minFrameRate + ", max: " + maxFrameRate + + " cur: " + framesToDraw + + " avg: " + String.format("%.1f", average) , 10, 10); + } + s.redraw(); + + // Mac need an additional redraw call. Else the animation stops and it reacts on user input. + e.display.timerExec(10, () -> { + if(!s.isDisposed()) { + s.redraw(); + } + }); + } + + private static void onResize(Event e, Canvas c) { + final var ca = c.getShell().getClientArea(); + c.setSize(ca.width, ca.height); + } + +} diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetText.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetText.java new file mode 100644 index 00000000000..3b2393885e5 --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetText.java @@ -0,0 +1,77 @@ +package org.eclipse.swt.examples.skia; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +public class SnippetText { + + public static void main(String[] args) { + Display display = new Display(); + Shell shell = new Shell(display); + shell.setText("SWT vs Skija drawText Example"); + shell.setSize(1100, 700); + shell.setLayout(new FillLayout(SWT.HORIZONTAL)); + + // Composite for two canvases side by side + org.eclipse.swt.widgets.Composite composite = shell; + + // Left: Classic SWT Canvas with scrollbars + ScrolledComposite scrolledClassic = new ScrolledComposite(composite, SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL); + Canvas canvasClassic = new Canvas(scrolledClassic, SWT.NONE); + scrolledClassic.setContent(canvasClassic); + scrolledClassic.setExpandHorizontal(true); + scrolledClassic.setExpandVertical(true); + canvasClassic.setSize(500, 1200); + scrolledClassic.setMinSize(canvasClassic.getSize()); + + // Right: Skija Canvas with scrollbars + ScrolledComposite scrolledSkija = new ScrolledComposite(composite, SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL); + Canvas canvasSkija = new Canvas(scrolledSkija, SWT.SKIA); + scrolledSkija.setContent(canvasSkija); + scrolledSkija.setExpandHorizontal(true); + scrolledSkija.setExpandVertical(true); + canvasSkija.setSize(500, 1200); + scrolledSkija.setMinSize(canvasSkija.getSize()); + + String multiLine = "Short\nA much &longer \ttab line &of text\rCarriage &return\nMid length\nTiny\nThe last line is the longest of all lines in this example.\nAmpersand: &File\nBackspace: A\bB\nForm feed: A\fB"; + Font font = new Font(display, "Arial", 15, SWT.NORMAL); + String displayText = multiLine.replace("\f", "\u240C"); + + // Paint logic for both canvases + java.util.function.Consumer paintLogic = gc -> { + gc.setFont(font); + gc.setBackground(display.getSystemColor(SWT.COLOR_YELLOW)); + int y = 5; + gc.drawText("draw\ttab", 20, y , SWT.DRAW_TAB); + y +=30; + gc.drawText("draw\rdeleimiter\r\ncarriagereturn", 20, y , SWT.DRAW_DELIMITER); + y +=70; + gc.drawText("draw\rtab and draw deleimiter\r\ncarriagereturn", 20, y , SWT.DRAW_DELIMITER | SWT.DRAW_TAB); + y +=80; + gc.drawText("Not draw\rtab", 20, y , SWT.NONE); + y +=30; + gc.drawText(displayText, 20, y); + y +=300; + gc.drawText(displayText, 20, y, true); + y +=300; + // windows always draws win delimiter mode, even without flag... + gc.drawText("SWT.DRAW_MNEMONIC: "+displayText, 20, y, SWT.DRAW_MNEMONIC); + y+=300; + }; + + canvasClassic.addPaintListener(e -> paintLogic.accept(e.gc)); + canvasSkija.addPaintListener(e -> paintLogic.accept(e.gc)); + + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) display.sleep(); + } + font.dispose(); + display.dispose(); + } +} \ No newline at end of file diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetTextExtent.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetTextExtent.java new file mode 100644 index 00000000000..fbfb0345578 --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetTextExtent.java @@ -0,0 +1,82 @@ +package org.eclipse.swt.examples.skia; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Shell; + +public class SnippetTextExtent { + public static void main(String[] args) { + Display display = new Display(); + Shell shell = new Shell(display); + shell.setText("Text Extent Snippet"); + shell.setLayout(new GridLayout(2, true)); + + // Create two canvases + for (int i = 0; i < 2; i++) { + int skiaStyle = (i == 0) ? SWT.NONE : SWT.SKIA; + Canvas canvas = new Canvas(shell, SWT.BORDER | skiaStyle); + canvas.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + canvas.addListener(SWT.Paint, new Listener() { + @Override + public void handleEvent(Event event) { + GC gc = event.gc; + Rectangle rect = canvas.getClientArea(); + Font font = new Font(display, "Arial", 250, SWT.BOLD); + gc.setFont(font); + String text = "a"; + // Use textExtent to get size + Point extent = gc.textExtent(text); + int x = rect.x + (rect.width - extent.x) / 2; + int y = rect.y + (rect.height - extent.y) / 2; + + // Print font metrics for debugging + org.eclipse.swt.graphics.FontMetrics metrics = gc.getFontMetrics(); + System.out.println((gc.getClass().getName().contains("GCExtension") ? "[SkiaGC]" : "[SWTGC]") + + " ascent=" + metrics.getAscent() + + ", descent=" + metrics.getDescent() + + ", leading=" + metrics.getLeading() + + ", height=" + metrics.getHeight() + + ", extentY=" + extent.y); + + + boolean red = false; + for (int i = 0; i < extent.y / 5; i = i + 5) { + if (red) { + gc.setBackground(display.getSystemColor(SWT.COLOR_RED)); + red = false; + } else { + gc.setBackground(display.getSystemColor(SWT.COLOR_GREEN)); + red = true; + } + gc.fillRectangle(x, y + i * 5, extent.x, 5); + } + gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK)); + gc.drawRectangle(new Rectangle(x, y, extent.x, extent.y)); + gc.drawString(text, x, y, true); + // Draw baseline indicator + int baselineY = y + metrics.getAscent(); + gc.setForeground(display.getSystemColor(SWT.COLOR_BLUE)); + gc.drawLine(x, baselineY, x + extent.x, baselineY); + font.dispose(); + } + }); + } + + shell.setSize(600, 300); + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) + display.sleep(); + } + display.dispose(); + } +} \ No newline at end of file diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetTextLayout.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetTextLayout.java new file mode 100644 index 00000000000..e9093be8fdc --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetTextLayout.java @@ -0,0 +1,51 @@ +package org.eclipse.swt.examples.skia; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.PaintEvent; +import org.eclipse.swt.events.PaintListener; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.TextLayout; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; + +public class SnippetTextLayout { + + private static final boolean useSkia = false; // Set to false to use default TextLayout + + public static void main(String[] args) { + Display display = new Display(); + Shell shell = new Shell(display); + shell.setText("TextLayout Example"); + shell.setLayout(new FillLayout()); + + int style = useSkia ? SWT.DOUBLE_BUFFERED | SWT.SKIA : SWT.DOUBLE_BUFFERED; + + Canvas canvas = new Canvas(shell, style); + canvas.addPaintListener(new PaintListener() { + @Override + public void paintControl(PaintEvent e) { + TextLayout layout = new TextLayout(display); + layout.setText("Hello, Skia TextLayout!\nMultiline and styled text."); + layout.setStyle(null, 0, 4); // Default style for 'Hello' + layout.setStyle(new org.eclipse.swt.graphics.TextStyle( + display.getSystemFont(), new Color(display, 0, 128, 255), null), 7, 15); // Style 'Skia Text' + + e.gc.setBackground(display.getSystemColor(SWT.COLOR_GREEN)); + + e.gc.fillRectangle(e.x, e.y, e.width, e.height); + + layout.draw(e.gc, 20, 20); + layout.dispose(); + } + }); + + shell.setSize(400, 200); + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) display.sleep(); + } + display.dispose(); + } +} \ No newline at end of file diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetTransformAndClipping.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetTransformAndClipping.java new file mode 100644 index 00000000000..791307e2461 --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetTransformAndClipping.java @@ -0,0 +1,66 @@ +package org.eclipse.swt.examples.skia; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Region; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Shell; + +public class SnippetTransformAndClipping { + + final static boolean USE_SKIA = true; // Set to true to use Skia rendering, false for default SWT rendering + + public static void main(String[] args) { + final Display display = new Display(); + final Shell shell = new Shell(display); + shell.setText("Snippet Transform"); + + var style = SWT.DOUBLE_BUFFERED | (USE_SKIA ? SWT.SKIA : SWT.NONE); + + final Canvas canvas = new Canvas(shell,style); + canvas.setSize(400, 400); + shell.setSize(420, 440); + + canvas.addListener(SWT.Paint, SnippetTransformAndClipping::onPaint); + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + } + } + display.dispose(); + } + + private static void onPaint(Event e) { + + e.gc.setClipping(5, 5, 20, 20); // Set clipping region to canvas size + e.gc.setBackground(e.display.getSystemColor(SWT.COLOR_DARK_GRAY)); + e.gc.fillRectangle(0, 0, 1000, 1000); + + e.gc.setClipping((Region)null); // Remove clipping region + + // Draw a blue rectangle without transform + e.gc.setBackground(e.display.getSystemColor(SWT.COLOR_BLUE)); + e.gc.fillRectangle(50, 50, 100, 60); + + // Apply a transform: translate and rotate + org.eclipse.swt.graphics.Transform transform = new org.eclipse.swt.graphics.Transform(e.display); + transform.rotate(30); // rotate 30 degrees + transform.translate(200, 200); // translate to (200, 200) + e.gc.setTransform(transform); + + + // Draw a red rectangle with transform + e.gc.setBackground(e.display.getSystemColor(SWT.COLOR_RED)); + e.gc.fillRectangle(0, 0, 100, 60); // Centered at the origin after transform + + e.gc.setClipping(50, 50, 20, 20); // Set clipping region to a small area + e.gc.setBackground(e.display.getSystemColor(SWT.COLOR_GREEN)); + e.gc.fillRectangle(0, 0, 1000, 1000); + + // Clean up + transform.dispose(); + e.gc.setTransform(null); // Reset to default + } +} \ No newline at end of file diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetTwoGlCanvasDrawing.java b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetTwoGlCanvasDrawing.java new file mode 100644 index 00000000000..a7f7aa8146a --- /dev/null +++ b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/SnippetTwoGlCanvasDrawing.java @@ -0,0 +1,140 @@ +package org.eclipse.swt.examples.skia; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Path; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.widgets.Canvas; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Shell; + +public class SnippetTwoGlCanvasDrawing { + + public static void main(String[] args) { + final Display display = new Display(); + final Shell shell = new Shell(display); + shell.setText("Snippet Two Gl Canvas"); + + final Canvas c1 = new Canvas(shell, SWT.DOUBLE_BUFFERED | SWT.H_SCROLL | SWT.SKIA); + final Canvas c2 = new Canvas(shell, SWT.DOUBLE_BUFFERED | SWT.H_SCROLL | SWT.SKIA); + + configureCanvas(shell, c1, 1, "SkiaGlCanvas"); + configureCanvas(shell, c2, 2, "SkiaGlCanvas2"); + + shell.setSize(1500, 1000); + + shell.open(); + while (!shell.isDisposed()) { + if (!display.readAndDispatch()) { + display.sleep(); + + } + } + display.dispose(); + } + + private static void configureCanvas(final Shell shell, final Canvas canvas, int index, String title) { + canvas.setSize(100, 100); + shell.addListener(SWT.Resize, e -> onResize(canvas, index)); + canvas.addListener(SWT.Paint, e -> onPaint(e, title)); + } + + private static void onPaint(Event e, String title) { + final Display d = e.widget.getDisplay(); + final GC gc = e.gc; + + gc.setForeground(d.getSystemColor(SWT.COLOR_RED)); + gc.drawRectangle(new Rectangle(0, 0, 100, 100)); + + gc.setForeground(d.getSystemColor(SWT.COLOR_BLUE)); + gc.drawRectangle(new Rectangle(100, 0, 100, 100)); + + gc.setForeground(d.getSystemColor(SWT.COLOR_GREEN)); + gc.drawRectangle(new Rectangle(0, 100, 100, 100)); + + final var img = d.getSystemImage(SWT.ICON_QUESTION); + gc.drawImage(img, 100, 100); + + gc.setForeground(d.getSystemColor(SWT.COLOR_RED)); + gc.drawLine(200, 100, 100, 200); + + gc.setForeground(d.getSystemColor(SWT.COLOR_CYAN)); + gc.drawFocus(200, 0, 100, 100); + + gc.setForeground(d.getSystemColor(SWT.COLOR_RED)); + gc.drawArc(200, 100, 100, 100, 90, 200); + + gc.setForeground(d.getSystemColor(SWT.COLOR_BLACK)); + gc.drawText(title, 100, 250); + + gc.setForeground(d.getSystemColor(SWT.COLOR_DARK_CYAN)); + gc.drawOval(0, 300, 100, 50); + + final Path p = new Path(d); + p.addRectangle(0, 400, 100, 100); + gc.setForeground(d.getSystemColor(SWT.COLOR_DARK_GREEN)); + gc.drawPath(p); + + gc.setForeground(d.getSystemColor(SWT.COLOR_GRAY)); + gc.drawRoundRectangle(0, 500, 100, 100, 50, 50); + + gc.setForeground(d.getSystemColor(SWT.COLOR_DARK_RED)); + gc.drawPoint(5, 600); + + final var bo = img.getBounds(); + gc.drawImage(img, 0, 0, bo.width / 2, bo.height / 2, 20, 600, bo.width * 2, bo.height * 2); + + gc.setForeground(d.getSystemColor(SWT.COLOR_DARK_MAGENTA)); + gc.drawText("Test\nTest2", 300, 0); + gc.drawText("Test2\nTest3", 300, 100, false); + gc.drawText("Transparent Text", 300, 200, true); + + gc.setForeground(d.getSystemColor(SWT.COLOR_DARK_GRAY)); + gc.drawPolygon(new int[] { 400, 2, // + 500, 100, // + 400, 100, // + 500, 2 // + }); + + gc.setForeground(d.getSystemColor(SWT.COLOR_DARK_YELLOW)); + gc.drawPolyline(new int[] { 400, 50, // + 500, 100, // + 600, 200 + + }); + + gc.setBackground(d.getSystemColor(SWT.COLOR_DARK_YELLOW)); + gc.fillArc(100, 300, 100, 100, 90, 200); + + gc.setBackground(d.getSystemColor(SWT.COLOR_DARK_GREEN)); + gc.fillGradientRectangle(100, 400, 100, 100, false); + + gc.fillOval(100, 500, 100, 50); + + final Path p2 = new Path(d); + p2.addRectangle(100, 600, 100, 100); + gc.fillPath(p2); + + gc.fillPolygon(new int[] { 100, 700, // + 200, 800, // + 100, 800, // + 200, 700 // + }); + + gc.fillRectangle(new Rectangle(100, 800, 100, 100)); + + gc.fillRoundRectangle(200, 800, 100, 100, 20, 20); + } + + private static void onResize(Canvas c, int index) { + final var ca = c.getShell().getClientArea(); + switch (index) { + case 1 -> c.setBounds(new Rectangle(0, 0, ca.width / 2, ca.height)); + case 2 -> c.setBounds(new Rectangle(ca.width / 2, 0, ca.width / 3, ca.height)); + default -> { + /* nothing to do */ } + } + } + +} diff --git a/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/home_nav.gif b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/home_nav.gif new file mode 100644 index 00000000000..4472e8ce5b3 Binary files /dev/null and b/examples/org.eclipse.swt.examples/src/org/eclipse/swt/examples/skia/home_nav.gif differ