js,jsdom: Canvas & context API; Added TypeSymbol.is_js_compatible & temporary hacks for JS ifaces (#12526)

pull/12533/head
playX 2021-11-20 22:28:11 +03:00 committed by GitHub
parent 258d0d6df7
commit 243e66a106
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 343 additions and 71 deletions

View File

@ -1,82 +1,90 @@
/*
import jsdom import jsdom
fn get_2dcontext(canvas jsdom.IElement) ?jsdom.CanvasRenderingContext2D { fn get_canvas(elem JS.HTMLElement) &JS.HTMLCanvasElement {
if canvas is jsdom.HTMLCanvasElement { match elem {
c := canvas.get_context('2d') JS.HTMLCanvasElement {
match c { return elem
jsdom.CanvasRenderingContext2D { }
return c else {
} panic('Not a canvas')
else {
return error('cannot fetch 2d context')
}
} }
} else {
return error('canvas is not an HTMLCanvasElement')
} }
} }
fn draw_line(context jsdom.CanvasRenderingContext2D, x1 int, y1 int, x2 int, y2 int) { fn draw_line(mut context JS.CanvasRenderingContext2D, x1 int, y1 int, x2 int, y2 int) {
context.begin_path() context.beginPath()
context.set_stroke_style('black') context.strokeStyle = 'black'.str
context.set_line_width(1) context.lineWidth = JS.Number(1)
context.move_to(x1, y1) context.moveTo(x1, y1)
context.line_to(x2, y2) context.lineTo(x2, y2)
context.stroke() context.stroke()
context.close_path() context.closePath()
} }
struct DrawState { struct DrawState {
mut: mut:
context JS.CanvasRenderingContext2D
drawing bool drawing bool
x int x int
y int y int
} }
fn main() { fn main() {
window := jsdom.window()
document := jsdom.document document := jsdom.document
clear_btn := document.getElementById('clearButton'.str) ?
elem := document.get_element_by_id('myButton') ? canvas_elem := document.getElementById('canvas'.str) ?
elemc := document.get_element_by_id('myCanvas') or { panic('no canvas') } canvas := get_canvas(canvas_elem)
canv := jsdom.get_html_canvas_element(elemc) or { panic('expected canvas') } ctx := canvas.getContext('2d'.str, js_undefined()) ?
context := match ctx {
context := canv.get_context_2d() JS.CanvasRenderingContext2D {
mut state := DrawState{} ctx
canv.add_event_listener('mousedown', fn [mut state] (_ jsdom.IEventTarget, event jsdom.IEvent) {
state.drawing = true
if event is jsdom.MouseEvent {
state.x = event.offset_x()
state.y = event.offset_y()
} }
}) else {
panic('can not get 2d context')
}
}
mut state := DrawState{context, false, 0, 0}
canv.add_event_listener('mousemove', fn [context, mut state] (_ jsdom.IEventTarget, event jsdom.IEvent) { canvas.addEventListener('mousedown'.str, fn [mut state] (event JS.Event) {
state.drawing = true
match event {
JS.MouseEvent {
state.x = int(event.offsetX)
state.y = int(event.offsetY)
}
else {}
}
}, JS.EventListenerOptions{})
canvas.addEventListener('mousemove'.str, fn [mut state] (event JS.Event) {
if state.drawing { if state.drawing {
if event is jsdom.MouseEvent { match event {
draw_line(context, state.x, state.y, event.offset_x(), event.offset_y()) JS.MouseEvent {
state.x = event.offset_x() draw_line(mut state.context, state.x, state.y, int(event.offsetX),
state.y = event.offset_y() int(event.offsetY))
state.x = int(event.offsetX)
state.y = int(event.offsetY)
}
else {}
} }
} }
}) }, JS.EventListenerOptions{})
jsdom.window.add_event_listener('mouseup', fn [context, mut state] (_ jsdom.IEventTarget, event jsdom.IEvent) { window.addEventListener('mouseup'.str, fn [mut state] (event JS.Event) {
if state.drawing { if state.drawing {
if event is jsdom.MouseEvent { match event {
draw_line(context, state.x, state.y, event.offset_x(), event.offset_y()) JS.MouseEvent {
draw_line(mut state.context, state.x, state.y, int(event.offsetX),
int(event.offsetY))
}
else {}
} }
state.x = 0 state.x = 0
state.y = 0 state.y = 0
state.drawing = false state.drawing = false
} }
}) }, JS.EventListenerOptions{})
elem.add_event_listener('click', fn [context, canv] (_ jsdom.IEventTarget, _ jsdom.IEvent) { clear_btn.addEventListener('click'.str, fn [mut state, canvas] (_ JS.Event) {
context.clear_rect(0, 0, canv.width(), canv.height()) state.context.clearRect(0, 0, canvas.width, canvas.height)
}) }, JS.EventListenerOptions{})
}
*/
fn main() {
panic('jsdom is being refactored; This example will be available soon')
} }

View File

@ -1,7 +1,7 @@
<body class="main"> <body class="main">
<title>Drawing with mouse events</title> <title>Drawing with mouse events</title>
<input type="button" id="myButton" value="Clear canvas"> <input type="button" id="clearButton" value="Clear canvas">
<canvas style="border: 1px solid black;" width="720" height="480" id="myCanvas"></canvas> <canvas style="border: 1px solid black;" width="720" height="480" id="canvas"></canvas>
<script type="text/javascript" src="draw.js"></script> <script type="text/javascript" src="draw.js"></script>
</body> </body>

View File

@ -57,6 +57,34 @@ pub interface JS.Map {
pub interface JS.Any {} pub interface JS.Any {}
pub fn js_is_null(x JS.Any) bool {
res := false
#res.val = x === null
return res
}
pub fn js_is_undefined(x JS.Any) bool {
res := false
#res.val = x === undefined
return res
}
pub fn js_null() JS.Any {
mut obj := JS.Any{}
#obj = null;
return obj
}
pub fn js_undefined() JS.Any {
mut obj := JS.Any{}
#obj = undefined;
return obj
}
pub interface JS.Array { pub interface JS.Array {
JS.Any // map(fn (JS.Any) JS.Any) JS.Array JS.Any // map(fn (JS.Any) JS.Any) JS.Array
map(JS.Any) JS.Array map(JS.Any) JS.Array

View File

@ -8,6 +8,13 @@ pub mut:
will_read_frequently bool will_read_frequently bool
} }
pub fn (settings CanvasRenderingContext2DSettings) to_js() JS.Any {
mut object := JS.Any{}
#object = { alpha: settings.alpha, colorSpace: settings.color_space.str, desynchronized: settings.desynchronized.val, willReadFrequently: settings.will_read_frequently.val };
return object
}
pub interface JS.DOMMatrix2DInit { pub interface JS.DOMMatrix2DInit {
mut: mut:
a JS.Number a JS.Number
@ -168,9 +175,9 @@ pub struct JS.EventListenerOptions {
} }
pub interface JS.EventTarget { pub interface JS.EventTarget {
addEventListener(cb EventCallback, options JS.EventListenerOptions) addEventListener(event JS.String, cb EventCallback, options JS.EventListenerOptions)
dispatchEvent(event JS.Event) JS.Boolean dispatchEvent(event JS.Event) JS.Boolean
removeEventListener(cb EventCallback, options JS.EventListenerOptions) removeEventListener(event JS.String, cb EventCallback, options JS.EventListenerOptions)
} }
// Event is an event which takes place in the DOM. // Event is an event which takes place in the DOM.
@ -320,6 +327,7 @@ pub interface JS.Document {
lastModified JS.String lastModified JS.String
inputEncoding JS.String inputEncoding JS.String
implementation JS.DOMImplementation implementation JS.DOMImplementation
getElementById(id JS.String) ?JS.HTMLElement
mut: mut:
bgColor JS.String bgColor JS.String
body JS.HTMLElement body JS.HTMLElement
@ -368,6 +376,14 @@ pub interface JS.Element {
scroll(x JS.Number, y JS.Number) scroll(x JS.Number, y JS.Number)
scrollBy(x JS.Number, y JS.Number) scrollBy(x JS.Number, y JS.Number)
toggleAttribute(qualifiedName JS.String, force JS.Boolean) JS.Boolean toggleAttribute(qualifiedName JS.String, force JS.Boolean) JS.Boolean
getElementsByClassName(className JS.String) JS.HTMLCollection
getElementsByTagName(qualifiedName JS.String) JS.HTMLCollection
getEelementsByTagNameNS(namespaecURI JS.String, localName JS.String) JS.HTMLCollection
hasAttribute(qualifiedName JS.String) JS.Boolean
hasAttributeNS(namespace JS.String, localName JS.String) JS.Boolean
hasAttributes() JS.Boolean
hasPointerCapture(pointerId JS.Number) JS.Boolean
matches(selectors JS.String) JS.Boolean
mut: mut:
className JS.String className JS.String
id JS.String id JS.String
@ -383,6 +399,13 @@ pub const (
document = JS.Document{} document = JS.Document{}
) )
pub fn window() JS.Window {
mut x := JS.Any(voidptr(0))
#x = window;
return x
}
fn init() { fn init() {
#jsdom__document = document; #jsdom__document = document;
} }
@ -398,3 +421,143 @@ pub fn event_listener(callback fn (JS.EventTarget, JS.Event)) EventCallback {
callback(target, event) callback(target, event)
} }
} }
pub interface JS.HTMLCollection {
length JS.Number
item(idx JS.Number) ?JS.Any
namedItem(name JS.String) ?JS.Any
}
pub interface JS.HTMLElement {
JS.Element
accessKeyLabel JS.String
offsetHeight JS.Number
offsetLeft JS.Number
offsetParent JS.Any
offsetTop JS.Number
offsetWidth JS.Number
click()
mut:
accessKey JS.String
autocapitalize JS.String
dir JS.String
draggable JS.Boolean
hidden JS.Boolean
innerText JS.String
lang JS.String
outerText JS.String
spellcheck JS.Boolean
title JS.String
translate JS.Boolean
}
pub fn JS.HTMLElement.prototype.constructor() JS.HTMLElement
pub interface JS.HTMLEmbedElement {
getSVGDocument() ?JS.Document
mut:
align JS.String
height JS.String
src JS.String
width JS.String
}
pub fn html_embed_type(embed JS.HTMLEmbedElement) JS.String {
mut str := JS.String{}
#str = embed.type
return str
}
pub fn JS.HTMLEmbedElement.prototype.constructor() JS.HTMLEmbedElement
pub type CanvasContext = JS.CanvasRenderingContext2D
| JS.WebGL2RenderingContext
| JS.WebGLRenderingContext
pub interface JS.HTMLCanvasElement {
JS.HTMLElement
getContext(contextId JS.String, options JS.Any) ?CanvasContext
mut:
height JS.Number
width JS.Number
}
pub type FillStyle = JS.CanvasGradient | JS.CanvasPattern | JS.String
pub interface JS.CanvasRenderingContext2D {
canvas JS.HTMLCanvasElement
beginPath()
clip(path JS.Path2D, fillRule JS.String)
fill(path JS.Path2D, fillRule JS.String)
isPointInPath(path JS.Path2D, x JS.Number, y JS.Number, fillRule JS.String) JS.Boolean
isPointInStroke(path JS.Path2D, x JS.Number, y JS.Number) JS.Boolean
stoke(path JS.Path2D)
createLinearGradient(x0 JS.Number, y0 JS.Number, x1 JS.Number, y1 JS.Number) JS.CanvasGradient
createRadialGradient(x0 JS.Number, y0 JS.Number, r0 JS.Number, x1 JS.Number, y1 JS.Number, r1 JS.Number) JS.CanvasGradient
createPattern(image JS.CanvasImageSource, repetition JS.String) ?JS.CanvasPattern
arc(x JS.Number, y JS.Number, radius JS.Number, startAngle JS.Number, endAngle JS.Number, counterclockwise JS.Boolean)
arcTo(x1 JS.Number, y1 JS.Number, x2 JS.Number, y2 JS.Number, radius JS.Number)
bezierCurveTo(cp1x JS.Number, cp1y JS.Number, cp2x JS.Number, cp2y JS.Number, x JS.Number, y JS.Number)
closePath()
ellipse(x JS.Number, y JS.Number, radiusX JS.Number, radiusY JS.Number, rotation JS.Number, startAngle JS.Number, endAngle JS.Number, counterclockwise JS.Boolean)
lineTo(x JS.Number, y JS.Number)
moveTo(x JS.Number, y JS.Number)
quadraticCurveTo(cpx JS.Number, cpy JS.Number, x JS.Number, y JS.Number)
rect(x JS.Number, y JS.Number, w JS.Number, h JS.Number)
getLineDash() JS.Array
setLineDash(segments JS.Array)
clearRect(x JS.Number, y JS.Number, w JS.Number, h JS.Number)
fillRect(x JS.Number, y JS.Number, w JS.null, h JS.Number)
strokeRect(x JS.Number, y JS.Number, w JS.Number, h JS.Number)
getTransformt() JS.DOMMatrix
resetTransform()
rotate(angle JS.Number)
scale(x JS.Number, y JS.Number)
setTransform(matrix JS.DOMMatrix)
transform(a JS.Number, b JS.Number, c JS.Number, d JS.Number, e JS.Number, f JS.Number)
translate(x JS.Number, y JS.Number)
drawFocusIfNeeded(path JS.Path2D, element JS.Element)
stroke()
mut:
lineCap JS.String
lineDashOffset JS.Number
lineJoin JS.String
lineWidth JS.Number
miterLimit JS.Number
fillStyle FillStyle
strokeStyle FillStyle
globalAlpha JS.Number
globalCompositeOperation JS.String
}
pub interface JS.CanvasGradient {
addColorStop(offset JS.Number, color JS.String)
}
pub interface JS.CanvasPattern {
setTransform(transform JS.DOMMatrix)
}
pub interface JS.WebGLRenderingContext {
canvas JS.HTMLCanvasElement
drawingBufferHeight JS.Number
drawingBufferWidth JS.Number
}
pub interface JS.WebGL2RenderingContext {
JS.WebGLRenderingContext
}
pub interface JS.Window {
JS.EventTarget
closed JS.Boolean
devicePixelRatio JS.Number
document JS.Document
frameElement JS.Element
innerHeight JS.Number
innerWidth JS.Number
length JS.Number
}
pub interface JS.Path2D {}

View File

@ -267,7 +267,12 @@ pub fn (t &Table) is_same_method(f &Fn, func &Fn) string {
for i in 0 .. f.params.len { for i in 0 .. f.params.len {
// don't check receiver for `.typ` // don't check receiver for `.typ`
has_unexpected_type := i > 0 && f.params[i].typ != func.params[i].typ has_unexpected_type := i > 0 && f.params[i].typ != func.params[i].typ
// temporary hack for JS ifaces
lsym := t.get_type_symbol(f.params[i].typ)
rsym := t.get_type_symbol(func.params[i].typ)
if lsym.language == .js && rsym.language == .js {
return ''
}
has_unexpected_mutability := !f.params[i].is_mut && func.params[i].is_mut has_unexpected_mutability := !f.params[i].is_mut && func.params[i].is_mut
if has_unexpected_type || has_unexpected_mutability { if has_unexpected_type || has_unexpected_mutability {

View File

@ -1247,6 +1247,34 @@ pub fn (t &TypeSymbol) find_method_with_generic_parent(name string) ?Fn {
return none return none
} }
// is_js_compatible returns true if type can be converted to JS type and from JS type back to V type
pub fn (t &TypeSymbol) is_js_compatible() bool {
mut table := global_table
if t.kind == .void {
return true
}
if t.kind == .function {
return true
}
if t.language == .js || t.name.starts_with('JS.') {
return true
}
match t.info {
SumType {
for variant in t.info.variants {
sym := table.get_final_type_symbol(variant)
if !sym.is_js_compatible() {
return false
}
}
return true
}
else {
return true
}
}
}
pub fn (t &TypeSymbol) str_method_info() (bool, bool, int) { pub fn (t &TypeSymbol) str_method_info() (bool, bool, int) {
mut has_str_method := false mut has_str_method := false
mut expects_ptr := false mut expects_ptr := false

View File

@ -413,8 +413,10 @@ pub fn (mut c Checker) sum_type_decl(node ast.SumTypeDecl) {
variant.pos) variant.pos)
} else if sym.kind in [.placeholder, .int_literal, .float_literal] { } else if sym.kind in [.placeholder, .int_literal, .float_literal] {
c.error('unknown type `$sym.name`', variant.pos) c.error('unknown type `$sym.name`', variant.pos)
} else if sym.kind == .interface_ { } else if sym.kind == .interface_ && sym.language != .js {
c.error('sum type cannot hold an interface', variant.pos) c.error('sum type cannot hold an interface', variant.pos)
} else if sym.kind == .struct_ && sym.language == .js {
c.error('sum type cannot hold an JS struct', variant.pos)
} }
if sym.name.trim_prefix(sym.mod + '.') == node.name { if sym.name.trim_prefix(sym.mod + '.') == node.name {
c.error('sum type cannot hold itself', variant.pos) c.error('sum type cannot hold itself', variant.pos)
@ -555,8 +557,7 @@ pub fn (mut c Checker) interface_decl(mut node ast.InterfaceDecl) {
c.ensure_type_exists(method.return_type, method.return_type_pos) or { return } c.ensure_type_exists(method.return_type, method.return_type_pos) or { return }
if is_js { if is_js {
mtyp := c.table.get_type_symbol(method.return_type) mtyp := c.table.get_type_symbol(method.return_type)
if (mtyp.language != .js && !method.return_type.is_void()) if !mtyp.is_js_compatible() {
&& !mtyp.name.starts_with('JS.') {
c.error('method $method.name returns non JS type', method.pos) c.error('method $method.name returns non JS type', method.pos)
} }
} }
@ -567,8 +568,7 @@ pub fn (mut c Checker) interface_decl(mut node ast.InterfaceDecl) {
c.ensure_type_exists(param.typ, param.pos) or { return } c.ensure_type_exists(param.typ, param.pos) or { return }
if is_js { if is_js {
ptyp := c.table.get_type_symbol(param.typ) ptyp := c.table.get_type_symbol(param.typ)
if (ptyp.kind != .function && ptyp.language != .js if !ptyp.is_js_compatible() && !(j == method.params.len - 1
&& !ptyp.name.starts_with('JS.')) && !(j == method.params.len - 1
&& method.is_variadic) { && method.is_variadic) {
c.error('method `$method.name` accepts non JS type as parameter', c.error('method `$method.name` accepts non JS type as parameter',
method.pos) method.pos)
@ -595,7 +595,7 @@ pub fn (mut c Checker) interface_decl(mut node ast.InterfaceDecl) {
c.ensure_type_exists(field.typ, field.pos) or { return } c.ensure_type_exists(field.typ, field.pos) or { return }
if is_js { if is_js {
tsym := c.table.get_type_symbol(field.typ) tsym := c.table.get_type_symbol(field.typ)
if tsym.language != .js && !tsym.name.starts_with('JS.') && tsym.kind != .function { if !tsym.is_js_compatible() {
c.error('field `$field.name` uses non JS type', field.pos) c.error('field `$field.name` uses non JS type', field.pos)
} }
} }
@ -5313,6 +5313,7 @@ pub fn (mut c Checker) expr(node ast.Expr) ast.Type {
defer { defer {
c.expr_level-- c.expr_level--
} }
// c.expr_level set to 150 so that stack overflow does not occur on windows // c.expr_level set to 150 so that stack overflow does not occur on windows
if c.expr_level > 150 { if c.expr_level > 150 {
c.error('checker: too many expr levels: $c.expr_level ', node.position()) c.error('checker: too many expr levels: $c.expr_level ', node.position())
@ -6407,6 +6408,7 @@ fn (mut c Checker) match_exprs(mut node ast.MatchExpr, cond_type_sym ast.TypeSym
} else { } else {
expr_type = expr_types[0].typ expr_type = expr_types[0].typ
} }
c.smartcast(node.cond, node.cond_type, expr_type, mut branch.scope) c.smartcast(node.cond, node.cond_type, expr_type, mut branch.scope)
} }
} }

View File

@ -620,8 +620,11 @@ fn (mut g JsGen) gen_method_decl(it ast.FnDecl, typ FnGenType) {
if is_varg { if is_varg {
g.writeln('$arg_name = new array(new array_buffer({arr: $arg_name,len: new int(${arg_name}.length),index_start: new int(0)}));') g.writeln('$arg_name = new array(new array_buffer({arr: $arg_name,len: new int(${arg_name}.length),index_start: new int(0)}));')
} else { } else {
if arg.typ.is_ptr() || arg.is_mut { asym := g.table.get_type_symbol(arg.typ)
g.writeln('$arg_name = new \$ref($arg_name)') if asym.kind != .interface_ && asym.language != .js {
if arg.typ.is_ptr() || arg.is_mut {
g.writeln('$arg_name = new \$ref($arg_name)')
}
} }
} }
} }
@ -743,7 +746,9 @@ fn (mut g JsGen) gen_anon_fn(mut fun ast.AnonFn) {
if is_varg { if is_varg {
g.writeln('$arg_name = new array(new array_buffer({arr: $arg_name,len: new int(${arg_name}.length),index_start: new int(0)}));') g.writeln('$arg_name = new array(new array_buffer({arr: $arg_name,len: new int(${arg_name}.length),index_start: new int(0)}));')
} else { } else {
if arg.typ.is_ptr() || arg.is_mut { asym := g.table.get_type_symbol(arg.typ)
if arg.typ.is_ptr() || (arg.is_mut && asym.kind != .interface_ && asym.language != .js) {
g.writeln('$arg_name = new \$ref($arg_name)') g.writeln('$arg_name = new \$ref($arg_name)')
} }
} }

View File

@ -2343,20 +2343,53 @@ fn (mut g JsGen) match_expr_sumtype(node ast.MatchExpr, is_expr bool, cond_var M
} else { } else {
g.write('if (') g.write('if (')
} }
if sym.kind == .sum_type || sym.kind == .interface_ {
x := branch.exprs[sumtype_index]
if x is ast.TypeNode {
typ := g.unwrap_generic(x.typ)
tsym := g.table.get_type_symbol(typ)
if tsym.language == .js && (tsym.name == 'JS.Number'
|| tsym.name == 'JS.Boolean' || tsym.name == 'JS.String') {
g.write('typeof ')
}
}
}
g.match_cond(cond_var) g.match_cond(cond_var)
if sym.kind == .sum_type { if sym.kind == .sum_type {
g.write(' instanceof ') x := branch.exprs[sumtype_index]
g.expr(branch.exprs[sumtype_index]) if x is ast.TypeNode {
typ := g.unwrap_generic(x.typ)
tsym := g.table.get_type_symbol(typ)
if tsym.language == .js && (tsym.name == 'JS.Number'
|| tsym.name == 'JS.Boolean' || tsym.name == 'JS.String') {
g.write(' === "${tsym.name[3..].to_lower()}"')
} else {
g.write(' instanceof ')
g.expr(branch.exprs[sumtype_index])
}
} else {
g.write(' instanceof ')
g.expr(branch.exprs[sumtype_index])
}
} else if sym.kind == .interface_ { } else if sym.kind == .interface_ {
if !sym.name.starts_with('JS.') { if !sym.name.starts_with('JS.') {
g.write('.val') g.write('.val')
} }
if branch.exprs[sumtype_index] is ast.TypeNode { x := branch.exprs[sumtype_index]
g.write(' instanceof ') if x is ast.TypeNode {
g.expr(branch.exprs[sumtype_index]) typ := g.unwrap_generic(x.typ)
tsym := g.table.get_type_symbol(typ)
if tsym.language == .js && (tsym.name == 'Number'
|| tsym.name == 'Boolean' || tsym.name == 'String') {
g.write(' === $tsym.name.to_lower()')
} else {
g.write(' instanceof ')
g.expr(branch.exprs[sumtype_index])
}
} else { } else {
g.write(' instanceof ') g.write(' instanceof ')
g.write('None__') g.write('None__')
} }
} }