From 243e66a106e3761026ef3b90f583e86e5ee852b5 Mon Sep 17 00:00:00 2001 From: playX Date: Sat, 20 Nov 2021 22:28:11 +0300 Subject: [PATCH] js,jsdom: Canvas & context API; Added TypeSymbol.is_js_compatible & temporary hacks for JS ifaces (#12526) --- examples/js_dom_draw/draw.js.v | 110 +++++++++++---------- examples/js_dom_draw/index.html | 4 +- vlib/builtin/js/jsfns.js.v | 28 ++++++ vlib/jsdom/jsdom.js.v | 167 +++++++++++++++++++++++++++++++- vlib/v/ast/table.v | 7 +- vlib/v/ast/types.v | 28 ++++++ vlib/v/checker/checker.v | 14 +-- vlib/v/gen/js/fn.v | 11 ++- vlib/v/gen/js/js.v | 45 +++++++-- 9 files changed, 343 insertions(+), 71 deletions(-) diff --git a/examples/js_dom_draw/draw.js.v b/examples/js_dom_draw/draw.js.v index efa88a1a1b..7add029c06 100644 --- a/examples/js_dom_draw/draw.js.v +++ b/examples/js_dom_draw/draw.js.v @@ -1,82 +1,90 @@ -/* import jsdom -fn get_2dcontext(canvas jsdom.IElement) ?jsdom.CanvasRenderingContext2D { - if canvas is jsdom.HTMLCanvasElement { - c := canvas.get_context('2d') - match c { - jsdom.CanvasRenderingContext2D { - return c - } - else { - return error('cannot fetch 2d context') - } +fn get_canvas(elem JS.HTMLElement) &JS.HTMLCanvasElement { + match elem { + JS.HTMLCanvasElement { + return elem + } + else { + panic('Not a canvas') } - } else { - return error('canvas is not an HTMLCanvasElement') } } -fn draw_line(context jsdom.CanvasRenderingContext2D, x1 int, y1 int, x2 int, y2 int) { - context.begin_path() - context.set_stroke_style('black') - context.set_line_width(1) - context.move_to(x1, y1) - context.line_to(x2, y2) +fn draw_line(mut context JS.CanvasRenderingContext2D, x1 int, y1 int, x2 int, y2 int) { + context.beginPath() + context.strokeStyle = 'black'.str + context.lineWidth = JS.Number(1) + context.moveTo(x1, y1) + context.lineTo(x2, y2) context.stroke() - context.close_path() + context.closePath() } struct DrawState { mut: + context JS.CanvasRenderingContext2D drawing bool x int y int } fn main() { + window := jsdom.window() document := jsdom.document - - elem := document.get_element_by_id('myButton') ? - elemc := document.get_element_by_id('myCanvas') or { panic('no canvas') } - canv := jsdom.get_html_canvas_element(elemc) or { panic('expected canvas') } - - context := canv.get_context_2d() - mut state := DrawState{} - 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() + clear_btn := document.getElementById('clearButton'.str) ? + canvas_elem := document.getElementById('canvas'.str) ? + canvas := get_canvas(canvas_elem) + ctx := canvas.getContext('2d'.str, js_undefined()) ? + context := match ctx { + JS.CanvasRenderingContext2D { + ctx } - }) + 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 event is jsdom.MouseEvent { - draw_line(context, state.x, state.y, event.offset_x(), event.offset_y()) - state.x = event.offset_x() - state.y = event.offset_y() + match event { + JS.MouseEvent { + draw_line(mut state.context, state.x, state.y, int(event.offsetX), + 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 event is jsdom.MouseEvent { - draw_line(context, state.x, state.y, event.offset_x(), event.offset_y()) + match event { + JS.MouseEvent { + draw_line(mut state.context, state.x, state.y, int(event.offsetX), + int(event.offsetY)) + } + else {} } state.x = 0 state.y = 0 state.drawing = false } - }) - elem.add_event_listener('click', fn [context, canv] (_ jsdom.IEventTarget, _ jsdom.IEvent) { - context.clear_rect(0, 0, canv.width(), canv.height()) - }) -} -*/ - -fn main() { - panic('jsdom is being refactored; This example will be available soon') + }, JS.EventListenerOptions{}) + clear_btn.addEventListener('click'.str, fn [mut state, canvas] (_ JS.Event) { + state.context.clearRect(0, 0, canvas.width, canvas.height) + }, JS.EventListenerOptions{}) } diff --git a/examples/js_dom_draw/index.html b/examples/js_dom_draw/index.html index 3d60688a34..bbf9e5cc69 100644 --- a/examples/js_dom_draw/index.html +++ b/examples/js_dom_draw/index.html @@ -1,7 +1,7 @@ Drawing with mouse events - - + + \ No newline at end of file diff --git a/vlib/builtin/js/jsfns.js.v b/vlib/builtin/js/jsfns.js.v index 5f9254c910..2b5d973dd3 100644 --- a/vlib/builtin/js/jsfns.js.v +++ b/vlib/builtin/js/jsfns.js.v @@ -57,6 +57,34 @@ pub interface JS.Map { 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 { JS.Any // map(fn (JS.Any) JS.Any) JS.Array map(JS.Any) JS.Array diff --git a/vlib/jsdom/jsdom.js.v b/vlib/jsdom/jsdom.js.v index 06c2d5483f..452b68efc1 100644 --- a/vlib/jsdom/jsdom.js.v +++ b/vlib/jsdom/jsdom.js.v @@ -8,6 +8,13 @@ pub mut: 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 { mut: a JS.Number @@ -168,9 +175,9 @@ pub struct JS.EventListenerOptions { } 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 - 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. @@ -320,6 +327,7 @@ pub interface JS.Document { lastModified JS.String inputEncoding JS.String implementation JS.DOMImplementation + getElementById(id JS.String) ?JS.HTMLElement mut: bgColor JS.String body JS.HTMLElement @@ -368,6 +376,14 @@ pub interface JS.Element { scroll(x JS.Number, y JS.Number) scrollBy(x JS.Number, y JS.Number) 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: className JS.String id JS.String @@ -383,6 +399,13 @@ pub const ( document = JS.Document{} ) +pub fn window() JS.Window { + mut x := JS.Any(voidptr(0)) + #x = window; + + return x +} + fn init() { #jsdom__document = document; } @@ -398,3 +421,143 @@ pub fn event_listener(callback fn (JS.EventTarget, JS.Event)) EventCallback { 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 {} diff --git a/vlib/v/ast/table.v b/vlib/v/ast/table.v index 6d6aa2cbf8..c56ac6b663 100644 --- a/vlib/v/ast/table.v +++ b/vlib/v/ast/table.v @@ -267,7 +267,12 @@ pub fn (t &Table) is_same_method(f &Fn, func &Fn) string { for i in 0 .. f.params.len { // don't check receiver for `.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 if has_unexpected_type || has_unexpected_mutability { diff --git a/vlib/v/ast/types.v b/vlib/v/ast/types.v index 87434bb47a..135d0e3994 100644 --- a/vlib/v/ast/types.v +++ b/vlib/v/ast/types.v @@ -1247,6 +1247,34 @@ pub fn (t &TypeSymbol) find_method_with_generic_parent(name string) ?Fn { 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) { mut has_str_method := false mut expects_ptr := false diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index f145233c12..8d4e9b9864 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -413,8 +413,10 @@ pub fn (mut c Checker) sum_type_decl(node ast.SumTypeDecl) { variant.pos) } else if sym.kind in [.placeholder, .int_literal, .float_literal] { 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) + } 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 { 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 } if is_js { mtyp := c.table.get_type_symbol(method.return_type) - if (mtyp.language != .js && !method.return_type.is_void()) - && !mtyp.name.starts_with('JS.') { + if !mtyp.is_js_compatible() { 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 } if is_js { ptyp := c.table.get_type_symbol(param.typ) - if (ptyp.kind != .function && ptyp.language != .js - && !ptyp.name.starts_with('JS.')) && !(j == method.params.len - 1 + if !ptyp.is_js_compatible() && !(j == method.params.len - 1 && method.is_variadic) { c.error('method `$method.name` accepts non JS type as parameter', 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 } if is_js { 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) } } @@ -5313,6 +5313,7 @@ pub fn (mut c Checker) expr(node ast.Expr) ast.Type { defer { c.expr_level-- } + // c.expr_level set to 150 so that stack overflow does not occur on windows if c.expr_level > 150 { 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 { expr_type = expr_types[0].typ } + c.smartcast(node.cond, node.cond_type, expr_type, mut branch.scope) } } diff --git a/vlib/v/gen/js/fn.v b/vlib/v/gen/js/fn.v index 5a8996ab60..c4f76ff46f 100644 --- a/vlib/v/gen/js/fn.v +++ b/vlib/v/gen/js/fn.v @@ -620,8 +620,11 @@ fn (mut g JsGen) gen_method_decl(it ast.FnDecl, typ FnGenType) { 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)}));') } else { - if arg.typ.is_ptr() || arg.is_mut { - g.writeln('$arg_name = new \$ref($arg_name)') + asym := g.table.get_type_symbol(arg.typ) + 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 { g.writeln('$arg_name = new array(new array_buffer({arr: $arg_name,len: new int(${arg_name}.length),index_start: new int(0)}));') } 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)') } } diff --git a/vlib/v/gen/js/js.v b/vlib/v/gen/js/js.v index ecb19f200d..89f3558074 100644 --- a/vlib/v/gen/js/js.v +++ b/vlib/v/gen/js/js.v @@ -2343,20 +2343,53 @@ fn (mut g JsGen) match_expr_sumtype(node ast.MatchExpr, is_expr bool, cond_var M } else { 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) if sym.kind == .sum_type { - g.write(' instanceof ') - g.expr(branch.exprs[sumtype_index]) + 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(' === "${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_ { if !sym.name.starts_with('JS.') { g.write('.val') } - if branch.exprs[sumtype_index] is ast.TypeNode { - g.write(' instanceof ') - g.expr(branch.exprs[sumtype_index]) + 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 == 'Number' + || tsym.name == 'Boolean' || tsym.name == 'String') { + g.write(' === $tsym.name.to_lower()') + } else { + g.write(' instanceof ') + g.expr(branch.exprs[sumtype_index]) + } } else { g.write(' instanceof ') - g.write('None__') } }