-
-
Notifications
You must be signed in to change notification settings - Fork 197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
!feat(components): add support for remote-images #796
Conversation
Oups GHA removed lockdir because esy can't solve dependencies. |
There's now a working example pushed, for now I created a I'm guessing we probably want a different API, so that will have to be handled either now or in the future. E.g. <Image src=Url("..", options) /> |
Can you see why this fails @Et7f3?
|
Ok, so still stuck at the above error.
Did you try to run it @Et7f3? (on your Windows-machine, if yes) 🙂 |
Work ok on my computer it manage to solve. I try to build. |
src/Draw/CanvasContext.re
Outdated
LetOperators.( | ||
{ | ||
let.map image = ImageRenderer.fromUrl(url); | ||
|
||
switch (image) { | ||
| None => () | ||
| Some(img) => | ||
Canvas.drawImageRect( | ||
v.canvas, | ||
img, | ||
None, | ||
Rect.makeLtrb(x, y, x +. width, y +. height), | ||
paint, | ||
) | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like the wrong place to do I/O, and asynchronous I/O in particular. How would you recover from error, provide fallback using a local file and such, for example, with this logic being here? This ought to be moved up to the component, I think.
src/UI_Primitives/Image.re
Outdated
node#setStyle(styles); | ||
node#setResizeMode(resizeMode); | ||
Obj.magic(node); | ||
module Asset = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I agree with this. Other props could be passed through the variant as well, if needed, e.g. Remote({url, timeout})
.
src/Draw/LetOperators.re
Outdated
let (let.map) = (promise, fn) => Lwt.map(fn, promise); | ||
let (let.mapOk) = (promise, fn) => | ||
Lwt.map( | ||
fun | ||
| Ok(response) => fn(response) | ||
| Error(e) => e, | ||
promise, | ||
); | ||
|
||
let (let.flatMapOk) = (promise, fn) => | ||
Lwt.bind( | ||
promise, | ||
fun | ||
| Ok(response) => fn(response) | ||
| Error(e) => Lwt.return(Error(e)), | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This ought to be moved to Utility
I think.
src/Draw/ImageRenderer.re
Outdated
let download = url => { | ||
Fetch.( | ||
{ | ||
Log.info("Fetching image: " ++ url); | ||
|
||
let.flatMapOk {Response.body, headers, _} = get(url); | ||
|
||
let data = Body.toString(body); | ||
let mediaType = | ||
headers | ||
|> List.find_opt(((k, _v)) => | ||
String.lowercase_ascii(k) == "content-type" | ||
) | ||
|> Option.map(((_k, v)) => v) | ||
|> Option.value(~default=""); | ||
|
||
Lwt.return(Ok({data, mediaType})); | ||
} | ||
); | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is also the wrong place to do I/O I think. A renderer should render, not do all kinds of other things as well. I can see how the earlier decision to have fromAssetPath
here lead to this, but adding network I/O and asynchrony makes the spaghetti bowl significantly deeper. I think this needs a proper refactoring to separate file I/O, asset handling, network access and rendering.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that probably makes sense! Any pointers on where you'd start? drawImage
takes a Skia.Image.t
and that we handle IO in the component? Would that mean that we need to create a wrapper-%component
for the Image-primitive perhaps?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... or would ImageNode work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe to begin with, I could start to put all the IO-specific things in a separate module and let drawImage
take:
let drawImage = (~x, ~y, ~width, ~height, ~paint=?, data: Skia.Image.t, v: t) => {
Canvas.drawImageRect(
v.canvas,
data,
None,
Rect.makeLtrb(x, y, x +. width, y +. height),
paint,
);
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that seems like a good start. Not sure what you mean with the wrapped component, but I don't think the component API needs or ought to change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've pushed some progress, let me know if you think this is the right direction and what else we could do to tidy things up. Really appreciate the feedback @glennsl! 👌
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great! I think this already addresses the bulk of my concerns. It would be nice to have the asset handling and networking separated out and to have proper interface files for the new modules. I'm also not sure about the idea of Image
living in IO
, but that's more of a cognitive dissonance issue than an actual problem I think.
Good job and thanks for doing this!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, I'll give another round soon just to tie things up. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added some interface-files and left a note that the Image
-module residing in IO
could be moved and/or have another API-surface. Agree with you on that!
After your feedback, I think this at least leaves it in a little better state than previously while still having room for improvements!
As for this PR to not have to compete against time and getting stale, I'm thinking we can remove the use of it in the examples (as well as the call to startEventLoop
), in order to enable Timber
again, but still get the functionality in. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I think that's a good idea if this can't be solved now. If possible, it might be a good idea to try to run Lwt
the same way we run Luv
in Oni2, polling on every tick so we're still in full control of the event loop:
I added some interface-files and left a note that the Image-module residing in IO could be moved and/or have another API-surface. Agree with you on that!
Good call. Thanks for adding the interfaces and comments! I find them very helpful.
Co-authored-by: Glenn Slotte <[email protected]>
src/IO/Image.rei
Outdated
/** | ||
* fromUrl | ||
* | ||
* Given a network file-path this returns a promise, | ||
* holding either a Ok(option(Skia.Image.t)) or an Error(string). | ||
* | ||
* Examples: | ||
* let result = Image.fromUrl("https://example.com/hello.png"); | ||
*/ | ||
let fromUrl: string => Lwt.t(result(option(Skia.Image.t), string)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why an option
inside a result
? What does Ok(None)
mean?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Changed in: 07edf87
Before bringing this in, as to not bring another breaking change too soon. The two things I thought about were:
Images downloaded from online would probably have an option for folder to cache in, delete/keep files after download, caching etc while these would not apply for images loaded from disk since they're already there. I don't think settings like these make a lot of sense as props, but better put in a record. If so, we can leave as is and the breaking change would be to go from: `Url(string) to something like: `Url({ path: string, ...options }) Which might be OK as a breaking change for the future? Unless we want to take pre-caution and let
Does images over network and file-images take the same props, I think they could, right? <Image
src=`Url({ ...Image.Url.defaults, url: "https://example.com/example.png" })
// src=`File("example.png")
onLoading={<View />}
onError={<View />}
/> If so, we can leave as is because it'll just be additional niceties/features in the future. |
I saw that you merged it @glennsl, but I think it's fine, the conclusions I came to are above 🙂 tl;dr I think the breaking change might go from Url(string) -> Url(Image.Url.t), rest of the props can be shared |
Ah, sorry, that was terrible timing.
This seems to me like an application-wide setting, not one that would need to be different for each component instance. Or to have the flexibility of having some instances with different settings, you could provide a functor interface for example.
For any optional props that aren't outright contradictory for file-images, I think that's absolutely fine. |
No worries at all, just hit me while discussing a bit with Zach!
Yep, that's true! |
A new PR to reflect changes from Skia.
Closes #537
BREAKING CHANGE:
When using the
<Image ... />
-primitive, you now have to pass a variant of either (`Url or `File) tosrc
.In other words, where you previously passed
<Image src="example.png" />
you'll now have to passor